delete the batch practice-problems module (superseded by practice sessions)
Owner ruling 2026-10-04: the module is a sunk cost, retrievable from history if ever revisited. Removed: core practice contracts + pipeline helpers, the PracticeStore port + adapter + data dir path, server service + API routes (sets, clusters picker, practice aborts), the practice pages + dialog/exam/list UI, chain-table's Generate handoff, sidebar nav, activity-dock practice rows, and the partitioner/ problemGenerator/grader model roles (BACKGROUND_MODEL_ROLES 8 → 5). Survivors, moved to core/chains.ts (pure + browser-safe): ReasoningChain, chainsForSelection, formatChainLine — the chain table and graph emission still consume them.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Selection-facing chain views — pure + browser-safe (the shell's chain
|
||||
* table consumes these; the barrel exports them). Born in the practice
|
||||
* contracts (2026-09-29), orphaned onto their own module when the batch
|
||||
* practice module was replaced by practice sessions (2026-10-04).
|
||||
*/
|
||||
|
||||
/** One reasoning chain — a selection-facing VIEW of stored follows-edges:
|
||||
* all edges into one conclusion, as premises → that conclusion. Singleton
|
||||
* chains (premises: []) carry edge-less nodes. */
|
||||
export interface ReasoningChain {
|
||||
/** The chain's identity — the CONCLUSION node's id (chains group by
|
||||
* conclusion, so the two are one). */
|
||||
id: string;
|
||||
/** Premise node ids — every follows-edge source into the conclusion,
|
||||
* sorted. Empty = a singleton chain (an edge-less node). */
|
||||
premises: string[];
|
||||
/** The conclusion node id (=== id; mirrored so renderings read
|
||||
* "premises → conclusion" without indirection). */
|
||||
conclusion: string;
|
||||
}
|
||||
|
||||
/** 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<{ id: string }, "id">>,
|
||||
chains: ReadonlyArray<Pick<{ id: string; premises: string[]; conclusion: string }, "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.
|
||||
* 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)`;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* Split by domain (src/core/contracts/): plan.ts (the session plan tree),
|
||||
* graph.ts (the global knowledge graph + its extraction pipeline), chat.ts
|
||||
* (session records + UI extension points), jobs.ts (the async sub-agent
|
||||
* protocol), practice.ts (the practice-problem system). Each domain file is
|
||||
* protocol), Each domain file is
|
||||
* owned outright by its stream; schema changes to a domain you don't own go
|
||||
* through master. This barrel keeps every existing import path
|
||||
* ("@/src/core" and "./contracts.ts") unchanged.
|
||||
@@ -16,4 +16,3 @@ export * from "./contracts/chat.ts";
|
||||
export * from "./contracts/jobs.ts";
|
||||
export * from "./contracts/probe.ts";
|
||||
export * from "./contracts/models.ts";
|
||||
export * from "./contracts/practice.ts";
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* message-granular; extraction is free-form and post-hoc, fully decoupled
|
||||
* from the session's plan tree. The graph is an extraction of the reasoning
|
||||
* chains from all teaching sessions (and imported notes) — NOTHING else:
|
||||
* no quiz outcomes, no understanding assessments (2026-09-27 cleanup —
|
||||
* understanding becomes practice-module territory). Anything not
|
||||
* no quiz outcomes, no understanding assessments (2026-09-27 cleanup).
|
||||
* Anything not
|
||||
* yet decided (the state model, strength derivation, selection strategies) is deliberately NOT here — transitional
|
||||
* stubs carry the renderer until those land.
|
||||
*
|
||||
@@ -289,26 +289,6 @@ export interface Cluster {
|
||||
members: string[];
|
||||
}
|
||||
|
||||
/** One birth-session cluster as the practice picker consumes it (owner
|
||||
* ruling 2026-09-29: problem sets take NODES — the UI resolves a picked
|
||||
* cluster to its member ids). EVERY birth-session group regardless of
|
||||
* size (owner ruling, same evening: MIN_CLUSTER_SIZE is a hull-drawing
|
||||
* rule — practice availability must not inherit a visual threshold);
|
||||
* `label` is null when the naming record is gone — honest, never
|
||||
* guessed (the graph view renders those unlabeled). */
|
||||
export interface GraphClusterGroup {
|
||||
/** The birth session's id — a chatId or a notes source id. */
|
||||
key: string;
|
||||
/** What kind of record names the group. */
|
||||
kind: "chat" | "notes";
|
||||
/** Session title / source file path; null when the record is gone. */
|
||||
label: string | null;
|
||||
/** Member node ids, sorted (the partition's members). */
|
||||
nodeIds: string[];
|
||||
/** === nodeIds.length, denormalized for the picker row. */
|
||||
nodeCount: number;
|
||||
}
|
||||
|
||||
export interface GraphSnapshot {
|
||||
/** Proposition nodes + the DERIVED implication nodes (one per chain,
|
||||
* id = chain id) — what the canvas renders. */
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
/**
|
||||
* Practice-problem contracts — the practice arc (feat/practice-problems,
|
||||
* plan locked 2026-09-29).
|
||||
*
|
||||
* PURPOSE (owner, 2026-09-29): strengthening, NOT assessment — maximize
|
||||
* strengthening per unit of mental effort. Teaching sessions install the
|
||||
* graph; practice widens the set of situations in which held propositions
|
||||
* come to mind. The old vision doc's exam machinery (assessment records,
|
||||
* understanding writes) is PARKED: grading is feedback-only and never
|
||||
* touches the graph.
|
||||
*
|
||||
* THE CHAIN MODEL (owner, 2026-09-29 — two rulings):
|
||||
* - Chains group by CONCLUSION: every follows-edge into a node forms ONE
|
||||
* chain (its premises → it). A conclusion of one chain can be a premise
|
||||
* of later chains; nodes may repeat across chains. Edge-less nodes are
|
||||
* singleton chains.
|
||||
* - FOLLOWS ONLY: supports edges are invisible to practice ("no edges")
|
||||
* — they neither chain nor cover.
|
||||
* The translation is a bijection: the union of premises→conclusion pairs
|
||||
* reconstructs exactly the selection's follows-edges (core/practice.ts).
|
||||
*
|
||||
* GENERATION is two-stage: a partitioner agent partitions the chains into
|
||||
* the smallest covering set of problems (dedup-aware, confusables grouped);
|
||||
* then ONE generator agent per problem builds it (the problem-engineering
|
||||
* framework as its doctrine). Numbered problems appear in the UI the
|
||||
* moment the partition lands — loading until their generator finishes —
|
||||
* and NEVER show which propositions they test. Each problem commits to its
|
||||
* full formulation, model solution, and grading rubric up front
|
||||
* (commit-before-pose, the sycophancy mitigation). GRADING is one
|
||||
* background job per problem, against the pre-written solution + rubric,
|
||||
* feedback-only.
|
||||
*
|
||||
* Part of the domain-split contracts (src/core/contracts/): this file is
|
||||
* owned by the practice stream. Pure types + protocol docs only.
|
||||
*/
|
||||
|
||||
/** One reasoning chain — the practice module's agent-facing unit: all
|
||||
* follows-edges into one conclusion, as premises → that conclusion.
|
||||
* Singleton chains (premises: []) carry edge-less nodes. */
|
||||
export interface ReasoningChain {
|
||||
/** The chain's identity — the CONCLUSION node's id (chains group by
|
||||
* conclusion, so the two are one). */
|
||||
id: string;
|
||||
/** Premise node ids — every follows-edge source into the conclusion,
|
||||
* sorted. Empty = a singleton chain (an edge-less node). */
|
||||
premises: string[];
|
||||
/** The conclusion node id (=== id; mirrored so agent-facing renderings
|
||||
* read "premises → conclusion" without indirection). */
|
||||
conclusion: string;
|
||||
}
|
||||
|
||||
/** The main axis, discretized (owner, 2026-09-29 — named levels, never
|
||||
* a number to interpolate; definitions verbatim from the ladder
|
||||
* doctrine): */
|
||||
export type LadderRung = 0 | 1 | 2 | 3 | 4;
|
||||
|
||||
/** Cue distance from every situation the learner has already met — the
|
||||
* lesson's own examples AND every previous problem's situation
|
||||
* (definitions owner-approved 2026-09-29): */
|
||||
export type CueDistance = 0 | 1 | 2;
|
||||
|
||||
/** How the situation is dressed — never changes the rung or the cue
|
||||
* distance: */
|
||||
export type Presentation = "theory" | "application";
|
||||
|
||||
/** The three settings every problem set is configured with (owner,
|
||||
* 2026-09-29) — each a named level, invoked as that level exactly:
|
||||
*
|
||||
* THE LADDER (the main axis, bottom to top):
|
||||
* 0 Worked example — the solution is shown; nothing is demanded. An
|
||||
* installation object; nothing yet to recall.
|
||||
* 1 Completion — the path is shown with a gap; the learner supplies
|
||||
* one step. What to recall is nearly announced.
|
||||
* 2 Direct exercise — one step, no hint, but the situation makes the
|
||||
* applicable proposition unmistakable. Trains execution, not choosing.
|
||||
* 3 Composition — several steps, chained; the path is narrow but must
|
||||
* be held and run.
|
||||
* 4 Path-finding — several steps, the path concealed, distractors
|
||||
* present, the statement suggesting wrong methods. The competition
|
||||
* shape.
|
||||
*
|
||||
* THE CUE DISTANCE:
|
||||
* 0 Lesson context — the textbook/toy-example shape in the teaching's
|
||||
* own framing.
|
||||
* 1 Adjacent context — a genuinely new situation built from the
|
||||
* teaching's own world — same domain, different setup/packaging.
|
||||
* 2 Far context — 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.
|
||||
*
|
||||
* THE PRESENTATION:
|
||||
* theory — the subject's own terms — symbols, formal objects, native
|
||||
* vocabulary.
|
||||
* application — a concrete worldly affair — a case, episode, system,
|
||||
* or story — in which the proposition's structure appears in the wild. */
|
||||
export interface PracticeParams {
|
||||
/** Which class of problem to build — exactly that rung. */
|
||||
ladderRung: LadderRung;
|
||||
/** How far the situation sits from the cues already met. */
|
||||
cueDistance: CueDistance;
|
||||
/** How the situation is dressed. */
|
||||
presentation: Presentation;
|
||||
}
|
||||
|
||||
/** Where a set stands in the two-stage run. The set is PERSISTED at
|
||||
* "partitioning" (before the partitioner runs), its problem stubs land
|
||||
* (still "generating") the moment the partition submits, and it flips to
|
||||
* "ready" when every generator has settled — errored problems included
|
||||
* (per-problem failure isolation: they carry their own status). */
|
||||
export type ProblemSetStatus = "partitioning" | "generating" | "ready" | "error";
|
||||
|
||||
/** One problem's own lifecycle — a stub from the partition until its
|
||||
* generator submits. */
|
||||
export type ProblemStatus = "generating" | "ready" | "error";
|
||||
|
||||
/** The grading job's deliverable (owner, 2026-09-29 — one attempt per
|
||||
* problem; the grader scores, it does not coach in prose): an integer
|
||||
* score against the pre-written rubric plus a minimal justification.
|
||||
* Nothing derives from it — no understanding writes. */
|
||||
export interface ProblemGrade {
|
||||
/** 0–10, integer. */
|
||||
score: number;
|
||||
/** Why this score — one or two sentences: which rubric points were
|
||||
* met or missed, and which proposition failed to come to mind. */
|
||||
reasoning: string;
|
||||
gradedAt: string;
|
||||
}
|
||||
|
||||
/** One practice problem — built by ONE generator agent from its assigned
|
||||
* chains, fully committed before the learner ever sees it. No name field
|
||||
* (owner, 2026-09-29): problems are numbered by position, nothing else. */
|
||||
export interface Problem {
|
||||
/** "pp_" + ts36 + rand — store-assigned. */
|
||||
id: string;
|
||||
/** Chain ids (ReasoningChain.id — the conclusion node ids) this problem
|
||||
* exercises. The coverage law: the union over all problems must equal
|
||||
* the set's chains. */
|
||||
chains: string[];
|
||||
/** The full problem formulation — world-facing; never names the
|
||||
* propositions it tests. */
|
||||
statement: string;
|
||||
/** The pre-written model solution (commit-before-pose). */
|
||||
solution: string;
|
||||
/** The brief free-form grading rubric — bullet points of what actually
|
||||
* matters. */
|
||||
rubric: string;
|
||||
status: ProblemStatus;
|
||||
/** Set when this problem's generation failed (the set completes
|
||||
* around it). */
|
||||
error?: string;
|
||||
/** The learner's answer, once submitted (dictation-first — thinking
|
||||
* out loud is the primary answering mode). */
|
||||
answer?: string;
|
||||
/** The grader's score + reasoning, once it lands. */
|
||||
grade?: ProblemGrade;
|
||||
}
|
||||
|
||||
/** One problem set — the persisted unit (one JSON document per set). */
|
||||
export interface ProblemSet {
|
||||
/** "ps_" + ts36 + rand — store-assigned. */
|
||||
id: string;
|
||||
/** Display label — the picked cluster's label, or the dated
|
||||
* default. */
|
||||
title: string;
|
||||
createdAt: string;
|
||||
/** Bumped on every store write — the live-run heartbeat the boot
|
||||
* sweep's grace window reads (a live run saves at every phase or
|
||||
* problem transition, so it is structurally never 120s stale).
|
||||
* Absent on legacy rows (validator tolerates; readers fall back
|
||||
* to createdAt). */
|
||||
updatedAt?: string;
|
||||
params: PracticeParams;
|
||||
/** The selected node ids — the raw selection record. The chain
|
||||
* decomposition derives from these + the follows-edges among them at
|
||||
* creation time. */
|
||||
nodeIds: string[];
|
||||
/** Snapshot of every selected proposition at creation — nodeId →
|
||||
* {name, statement}. The set is SELF-CONTAINED (planning ruling,
|
||||
* 2026-09-29): rendering, generation, grading, and dedup never
|
||||
* re-read the live graph, which keeps moving (extraction continues,
|
||||
* nodes get deleted); a set is a document of what WAS selected. */
|
||||
propositions: Record<string, { name: string; statement: string }>;
|
||||
/** The frozen chain derivation (chainsForSelection at creation time).
|
||||
* Coverage checks and the problems' chain tags resolve against THIS,
|
||||
* never against live edges. */
|
||||
chains: ReasoningChain[];
|
||||
status: ProblemSetStatus;
|
||||
/** Set when the partition itself (or the run as a whole) failed. */
|
||||
error?: string;
|
||||
/** The learner's free-form note from the creation dialog (owner,
|
||||
* 2026-09-29: more learner control, no other purpose). Carried to the
|
||||
* partitioner and every generator as ADDITIONAL INPUT — weighed as
|
||||
* preference, never an obligation, never overriding the laws. Trimmed,
|
||||
* capped (2000 chars at the API); persisted so the set stays a
|
||||
* self-contained document of what was asked for. */
|
||||
note?: string;
|
||||
/** Problem stubs appear here the moment the partitioner submits —
|
||||
* numbered by position, "generating", propositions invisible. */
|
||||
problems: Problem[];
|
||||
}
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* One bus, every layer: server emits turn lifecycle here, the store
|
||||
* adapters emit job/chat mutations, the agent layer's background
|
||||
* registries (extraction, practice) emit activity changes — and
|
||||
* registries (extraction) emit activity changes — and
|
||||
* GET /api/events turns every emission into an SSE frame for any open
|
||||
* window. The events carry the FACT, never the data: consumers fetch
|
||||
* truth on receipt (push the poke, pull the data), so payloads stay
|
||||
@@ -34,7 +34,7 @@ export interface ServerEvents {
|
||||
/** A job record changed (dispatch, progress, settle, abort, recovery).
|
||||
* chatId when known — consumers that own one chat filter on it. */
|
||||
"jobs-changed": { chatId?: string };
|
||||
/** An extraction or practice run changed phase (activity dock). */
|
||||
/** An extraction run changed phase (activity dock). */
|
||||
"activity-changed": Record<string, never>;
|
||||
/** The session list changed (create, delete, title). */
|
||||
"chats-changed": Record<string, never>;
|
||||
|
||||
+7
-4
@@ -48,7 +48,8 @@
|
||||
*/
|
||||
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { CoverageEntry } from "./contracts/graph.ts";
|
||||
import type { CoverageEntry,
|
||||
} from "./contracts/graph.ts";
|
||||
import type {
|
||||
AuditVerdicts,
|
||||
ExtractedChain,
|
||||
@@ -62,8 +63,8 @@ import type {
|
||||
GraphSnapshot,
|
||||
} from "./contracts.ts";
|
||||
import { MIN_CLUSTER_SIZE } from "./clusters.ts";
|
||||
import { formatChainLine } from "./chains.ts";
|
||||
import { attachImpToCommunities, birthSessionOf, birthSessionPartition } from "./session-clusters.ts";
|
||||
import { formatChainLine } from "./practice.ts";
|
||||
import type { ChatStore, GraphStore } from "./ports.ts";
|
||||
|
||||
/** The storage ports every graph operation needs, passed explicitly. */
|
||||
@@ -500,7 +501,7 @@ export function graphSnapshot(deps: GraphDeps): GraphSnapshot {
|
||||
const graph = getGraph(deps.graphStore);
|
||||
// The canvas-facing lossy image: propositions + derived implication
|
||||
// nodes, untyped arrows. `chains` (the stored truth) ride along for
|
||||
// the chain table and the practice pipeline — they read the truth,
|
||||
// the chain table — it reads the truth, never the derived fan-out,
|
||||
// never the derived fan-out.
|
||||
const view = deriveView(graph);
|
||||
const nodes = view.nodes;
|
||||
@@ -657,7 +658,7 @@ export function graphSessionIndex(deps: GraphDeps): GraphSessionEntry[] {
|
||||
}
|
||||
|
||||
/** One birth proposition of one session — WHAT WAS COVERED there (2026-09-27
|
||||
* cleanup: no understanding levels; assessment is practice-module
|
||||
* cleanup: no understanding levels; assessment is
|
||||
* territory and nothing writes it until that module exists). */
|
||||
export interface GraphSessionProposition {
|
||||
nodeId: string;
|
||||
@@ -676,3 +677,5 @@ export function graphSessionPropositions(deps: GraphDeps, sessionId: string): Gr
|
||||
|
||||
// Re-export: the contract owns the shape; legacy importers still reach it.
|
||||
export type { EmissionStats };
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -17,6 +17,7 @@
|
||||
|
||||
export * from "./contracts.ts";
|
||||
export * from "./ports.ts";
|
||||
export * from "./chains.ts";
|
||||
export * from "./render-settings.ts";
|
||||
|
||||
export * from "./question-core.ts";
|
||||
@@ -35,7 +36,6 @@ export * from "./phases.ts";
|
||||
export * from "./pending.ts";
|
||||
export * from "./prompt-segments.ts";
|
||||
export * from "./math-delims.ts";
|
||||
export * from "./practice.ts";
|
||||
// Wave B (owner-approved 2026-09-26): the native record (LhMessage), the
|
||||
// and the input-events contract — all
|
||||
// pure logic, browser-safe.
|
||||
|
||||
@@ -21,7 +21,6 @@ import type {
|
||||
ExtractionRunUsage,
|
||||
PlanTree,
|
||||
Position,
|
||||
ProblemSet,
|
||||
CoverageEntry,
|
||||
SessionCheckpoint,
|
||||
TurnUsage,
|
||||
@@ -157,33 +156,6 @@ export interface JobStore {
|
||||
deleteJobsForChat(chatId: string): number;
|
||||
}
|
||||
|
||||
// ─── PracticeStore — problem-set documents (data/practice) ──────────────────
|
||||
|
||||
/** Everything a new set needs except the store-assigned "ps_" id. */
|
||||
export type CreateProblemSetInit = Omit<ProblemSet, "id">;
|
||||
|
||||
/**
|
||||
* The practice module's persistence: one JSON document per problem set
|
||||
* (src/store/practice.ts adapter). Sets are bounded, SELF-CONTAINED
|
||||
* snapshots — propositions + chains frozen at creation (contracts/
|
||||
* practice.ts) — so reads never join the live graph. Replace-whole
|
||||
* writes like settings.json, one-file-per-record like data/jobs.
|
||||
* Concurrent same-set writes are the pipeline's serialization duty
|
||||
* (slice 4), not the store's — this seam owns file integrity only.
|
||||
*/
|
||||
export interface PracticeStore {
|
||||
/** Persist a new set; the store assigns the id ("ps_" + ts36 + rand). */
|
||||
createSet(init: CreateProblemSetInit): ProblemSet;
|
||||
/** The full set document, or null when unknown id / corrupt file. */
|
||||
getSet(id: string): ProblemSet | null;
|
||||
/** Every set, createdAt descending (the practice page's list view). */
|
||||
listSets(): ProblemSet[];
|
||||
/** Persist the full set document (replace-whole). */
|
||||
saveSet(set: ProblemSet): void;
|
||||
/** Delete a set file; false when unknown id. */
|
||||
deleteSet(id: string): boolean;
|
||||
}
|
||||
|
||||
// ─── SettingsStore — THE user settings record (settings-storage wave, 2026-09-19) ──
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* 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() };
|
||||
}
|
||||
@@ -46,9 +46,6 @@ export const BACKGROUND_MODEL_ROLES = [
|
||||
"visualMaker",
|
||||
"extractor",
|
||||
"auditor",
|
||||
"partitioner",
|
||||
"problemGenerator",
|
||||
"grader",
|
||||
] as const;
|
||||
|
||||
export type BackgroundModelRole = (typeof BACKGROUND_MODEL_ROLES)[number];
|
||||
@@ -229,12 +226,6 @@ export const ROLE_SLOT: Readonly<Record<ModelRole, ModelSlotName>> = {
|
||||
auditor: "cheap",
|
||||
visualMaker: "vision",
|
||||
pdfReader: "vision",
|
||||
// The practice trio (2026-09-29): problem engineering is the product's
|
||||
// quality-critical component — strong tier for partitioner/generator/
|
||||
// grader alike (owner-approved plan default; one edit away from cheap).
|
||||
partitioner: "strong",
|
||||
problemGenerator: "strong",
|
||||
grader: "strong",
|
||||
};
|
||||
|
||||
/** General settings — app-level behavior. Version DISPLAY and plans login
|
||||
|
||||
Reference in New Issue
Block a user