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:
amosblomqvist
2026-10-04 21:34:24 +03:00
parent a2c1287415
commit 814c2ad412
50 changed files with 125 additions and 5767 deletions
+67
View File
@@ -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)`;
}
+1 -2
View File
@@ -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";
+2 -22
View File
@@ -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. */
-200
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
-28
View File
@@ -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) ──
/**
-119
View File
@@ -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() };
}
-9
View File
@@ -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