From d47b3564cebd806a8f6744f0bd2c1306bd03f665 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Mon, 5 Oct 2026 06:26:21 +0300 Subject: [PATCH] practice: resolveTargetId + ids-first context marker (round 9 slice 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- next-env.d.ts | 4 ++-- src/core/chains.ts | 23 +++++++++++++++++++++++ src/core/native-messages.ts | 9 +++++++-- tests/graph-context.test.ts | 8 +++++--- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index ce4e94a..a419cbe 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/types/routes.d.ts"; -import "./.next/types/root-params.d.ts"; +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/core/chains.ts b/src/core/chains.ts index ec716f7..9d23a6c 100644 --- a/src/core/chains.ts +++ b/src/core/chains.ts @@ -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>, +): 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, propositions: Readonly>, diff --git a/src/core/native-messages.ts b/src/core/native-messages.ts index de0c005..40d0dd8 100644 --- a/src/core/native-messages.ts +++ b/src/core/native-messages.ts @@ -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}`)); diff --git a/tests/graph-context.test.ts b/tests/graph-context.test.ts index d999c17..4bf46c5 100644 --- a/tests/graph-context.test.ts +++ b/tests/graph-context.test.ts @@ -49,7 +49,9 @@ describe("newGraphContextMessage", () => { const m = newGraphContextMessage(PICKS, ["Modus ponens → Soundness"]); assert.equal(m.customType, "graph-context"); assert.match(m.content, /Contextualization — the learner picked these propositions/); - assert.ok(m.content.includes("- Modus ponens: If P implies Q and P holds, Q holds.")); + // IDS FIRST (owner round 9): every pick carries its id — the problem + // tool's target field references these; names alone no longer. + assert.ok(m.content.includes(`- [${PICKS[0]!.id}] Modus ponens: If P implies Q and P holds, Q holds.`)); assert.ok(m.content.includes("Reasoning chains among them")); assert.ok(m.content.includes("- Modus ponens → Soundness")); // the UI payload is the structured pick, not the flattened text @@ -133,8 +135,8 @@ describe("POST /api/chats — graphContext becomes message[0]", () => { const m = record.messages[0] as { role: string; customType: string; content: string }; assert.equal(m.role, "custom"); assert.equal(m.customType, "graph-context"); - assert.ok(m.content.includes("- Modus ponens: If P implies Q and P holds, Q holds.")); - // the chain AMONG the picks rides the marker (formatChainLine shape) + assert.ok(m.content.includes("- [n_a] Modus ponens: If P implies Q and P holds, Q holds.")); + // the chain AMONG the picks rides the marker (formatChainLine shape — names) assert.ok(m.content.includes("- Modus ponens → Soundness")); });