practice: resolveTargetId + ids-first context marker (round 9 slice 1)
The mastery-trace contract: problem targets must be traceable to propositions. resolveTargetId (pure, chains.ts) resolves a target string to a canonical node id — id exact first, then name case-insensitive — against the WHOLE graph (pickless sessions and future graph-tools poses stay valid). The graph-context marker now introduces each pick as [id] name: statement (ids were never shown to the model before — names only); chains stay names: comprehension context, not tags.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { GraphNode } from "./contracts/graph.ts";
|
||||
|
||||
/**
|
||||
* Selection-facing chain views — pure + browser-safe (the shell's chain
|
||||
* table consumes these; the barrel exports them). Born in the practice
|
||||
@@ -55,6 +57,27 @@ export function chainsForSelection(
|
||||
* 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. */
|
||||
/** Resolve ONE target string to a canonical proposition id (owner round
|
||||
* 9, 2026-10-05 — the mastery-trace contract): id first (exact), then
|
||||
* name (case-insensitive, trimmed — names are presentational handles,
|
||||
* not unique; an ambiguous name resolves to the FIRST node in store
|
||||
* order). Returns null when nothing matches — the caller rejects.
|
||||
* Validation set is the WHOLE graph, not the session's picks: pickless
|
||||
* sessions and future graph-tools poses stay valid. */
|
||||
export function resolveTargetId(
|
||||
target: string,
|
||||
nodes: ReadonlyArray<Pick<GraphNode, "id" | "name">>,
|
||||
): string | null {
|
||||
const t = target.trim();
|
||||
if (t.length === 0) return null;
|
||||
if (nodes.some((n) => n.id === t)) return t;
|
||||
const lower = t.toLowerCase();
|
||||
for (const n of nodes) {
|
||||
if (n.name.trim().toLowerCase() === lower) return n.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatChainLine(
|
||||
chain: Pick<ReasoningChain, "premises" | "conclusion">,
|
||||
propositions: Readonly<Record<string, { name: string }>>,
|
||||
|
||||
@@ -203,11 +203,16 @@ export interface GraphContextPick {
|
||||
* feeds the transcript's collapsible GraphContextCard. No more seed
|
||||
* text glued into the first prompt. */
|
||||
export function newGraphContextMessage(picks: readonly GraphContextPick[], chains: readonly string[]): LhMarkerMessage {
|
||||
// IDS FIRST (owner round 9, 2026-10-05 — the mastery-trace contract):
|
||||
// each proposition is introduced ONCE with its id, name, and statement;
|
||||
// the problem tool's `target` field takes these ids (names normalize to
|
||||
// ids at the seam). Chains below stay NAMES — they are comprehension
|
||||
// context, not tags; names read, ids reference.
|
||||
const lines = [
|
||||
"Contextualization — the learner picked these propositions from their knowledge map when starting this session:",
|
||||
"",
|
||||
"Propositions:",
|
||||
...picks.map((p) => `- ${p.name}: ${p.statement}`),
|
||||
"Propositions (id — name: statement; the problem tool's target field uses these ids):",
|
||||
...picks.map((p) => `- [${p.id}] ${p.name}: ${p.statement}`),
|
||||
];
|
||||
if (chains.length > 0) {
|
||||
lines.push("", "Reasoning chains among them (premises → conclusion):", ...chains.map((c) => `- ${c}`));
|
||||
|
||||
Reference in New Issue
Block a user