Files
learnharness/src/core/practice.ts
T
amosblomqvist 4985b6f9fa chains-primary: stored chains replace typed edges; edges become a derived view
Owner rulings 2026-09-30: reasoning chains are the stored truth; the graph
canvas renders a lossy derived image. No migration, no backcompat — old
graph data was wiped.

- contracts: GraphChain (arg_-minted id, premises, conclusion) replaces
  typed edges; GraphEdge survives only as the untyped derived arrow;
  GraphSnapshot carries stored chains; supports edges retired entirely
- store: chains.jsonl ledger (edges.jsonl purged at load alongside legacy
  events.jsonl); readChains/appendChain seam; merge dedupes by
  (sorted premises, conclusion), re-assertions append under the same arg_ id
- core: deriveView mints implication nodes (name ⟹, statement via
  formatChainLine) plus premise→imp→conclusion arrows; clusters stay
  proposition-only (birthSessionPartition); chainsForSelection now groups
  STORED chains (chain rides iff every member selected) — independent
  arguments for one conclusion never union; chainsToGraph/chainsToEdges die
- extraction: extractor submits chains natively (expandFindings deleted);
  auditor receives propositions only; supports fan-out gone
- deletion: full death of a proposition kills every referencing chain;
  arg_-prefixed node deletes kill that chain; chainsRemoved stats
- UI: every arrow renders the ⟹ double shaft; supports toggle + thickness
  slider and detail-card edge sections deleted (card keeps name +
  proposition); chain table + practice pipeline read stored chains
- tests: 1188 green across the new seams
2026-09-30 03:22:52 +03:00

120 lines
6.2 KiB
TypeScript

/**
* Practice-module core — the chain bijection + coverage (slice 1 of the
* 2026-09-29 plan). Pure, no I/O.
*
* chainsForSelection(): the stored CHAINS (chains-primary, owner
* 2026-09-30) filtered to a selection — a chain rides along iff ALL of
* its members are selected; nodes in no surviving chain become singleton
* chains. formatChainLine(): one world-facing line per chain.
* chainCoverage(): the set's completeness law — the union of the
* problems' chains must equal the set's chains; the partitioner's submit
* is validated against it.
*/
import type { CueDistance, GraphChain, GraphNode, LadderRung, Presentation, Problem, ReasoningChain } from "./contracts.ts";
/** The named levels (owner-approved 2026-09-29) — definitions verbatim
* from the ladder doctrine / the contract's tables. The UI's stepped
* controls, the exam header's levels line, and the agents' prompt
* rendering all speak these names — never a bare number. */
export const LADDER_RUNGS: Record<LadderRung, { name: string; definition: string }> = {
0: { name: "Worked example", definition: "the solution is shown; nothing is demanded. An installation object; nothing yet to recall." },
1: { name: "Completion", definition: "the path is shown with a gap; the learner supplies one step. What to recall is nearly announced." },
2: { name: "Direct exercise", definition: "one step, no hint, but the situation makes the applicable proposition unmistakable. Trains execution, not choosing." },
3: { name: "Composition", definition: "several steps, chained; the path is narrow but must be held and run." },
4: { name: "Path-finding", definition: "several steps, the path concealed, distractors present, the statement suggesting wrong methods. The competition shape." },
};
export const CUE_LEVELS: Record<CueDistance, { name: string; definition: string }> = {
0: { name: "Lesson context", definition: "the textbook/toy-example shape in the teaching's own framing." },
1: { name: "Adjacent context", definition: "a genuinely new situation built from the teaching's own world — same domain, different setup/packaging." },
2: { name: "Far context", definition: "as far from the typical cue as recognition allows, the edge of the recognizable band — the proposition must still genuinely apply, and the learner must be able to find it by looking." },
};
export const PRESENTATIONS: Record<Presentation, { name: string; definition: string }> = {
theory: { name: "Theory", definition: "the subject's own terms — symbols, formal objects, native vocabulary." },
application: { name: "Application", definition: "a concrete worldly affair — a case, episode, system, or story — in which the proposition's structure appears in the wild." },
};
/** The stored chains filtered to a selection (chains-primary, owner
* 2026-09-30): a chain rides along iff its conclusion AND every premise
* are selected; a node in no surviving chain is a singleton chain
* (premises: []). Premise-only nodes whose chain was excluded are
* singletons — the same inclusion law as the pre-chains edge grouping.
* Every stored chain keeps its OWN id (arg_…) — two independent
* arguments for one conclusion stay distinct. Deterministic: chains
* sorted by id, premises sorted. */
export function chainsForSelection(
nodes: ReadonlyArray<Pick<GraphNode, "id">>,
chains: ReadonlyArray<Pick<GraphChain, "id" | "premises" | "conclusion">>,
): ReasoningChain[] {
const selected = new Set(nodes.map((n) => n.id));
const chained = new Set<string>();
const included: ReasoningChain[] = [];
for (const chain of chains) {
if (!selected.has(chain.conclusion)) continue;
if (!chain.premises.every((premise) => selected.has(premise))) continue;
included.push({ id: chain.id, premises: [...chain.premises].sort(), conclusion: chain.conclusion });
chained.add(chain.conclusion);
for (const premise of chain.premises) chained.add(premise);
}
for (const node of nodes) {
if (!chained.has(node.id)) {
included.push({ id: node.id, premises: [], conclusion: node.id });
}
}
return included.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
}
/** A chain as one world-facing line — the propositions' NAMES joined
* by the derivation arrow: "Premise one, Premise two → Conclusion"; a
* singleton shows its "(no premises)" marker. Pure + browser-safe:
* the shell (chain table, derived previews) cannot import the agent
* module — the level tables set this precedent. Unknown ids fall back
* to the raw id (honest, never guessed). Premises sort by id — the
* same determinism law as chainsForSelection. */
export function formatChainLine(
chain: Pick<ReasoningChain, "premises" | "conclusion">,
propositions: Readonly<Record<string, { name: string }>>,
): string {
const nameOf = (id: string) => propositions[id]?.name ?? id;
const premises = [...chain.premises].sort().map(nameOf).join(", ");
return premises !== ""
? `${premises} → ${nameOf(chain.conclusion)}`
: `${nameOf(chain.conclusion)} (no premises)`;
}
/** chainCoverage()'s report. `missing` fails the partition; `duplicated`
* is informational — a chain MAY legitimately be exercised by several
* problems (the operations make that sound). */
export interface ChainCoverageReport {
/** Chain ids no problem exercises — a partition leaving any is invalid. */
missing: string[];
/** Chain ids exercised by more than one problem (allowed). */
duplicated: string[];
}
/** The set's completeness law: every chain of the input must be exercised
* by the union of the problems' chains. Pure — the partitioner's submit
* validation (slice 3) and the tests call this. */
export function chainCoverage(
set: {
chains: ReadonlyArray<Pick<ReasoningChain, "id">>;
problems: ReadonlyArray<Pick<Problem, "chains">>;
},
): ChainCoverageReport {
const counts = new Map<string, number>();
for (const problem of set.problems) {
for (const chainId of problem.chains) {
counts.set(chainId, (counts.get(chainId) ?? 0) + 1);
}
}
const missing: string[] = [];
const duplicated: string[] = [];
for (const chain of set.chains) {
const count = counts.get(chain.id) ?? 0;
if (count === 0) missing.push(chain.id);
else if (count > 1) duplicated.push(chain.id);
}
return { missing: missing.sort(), duplicated: duplicated.sort() };
}