practice: target-validation pins + the id convention in the base prompt (round 9 slice 3)

Four pins on the mastery-trace contract: a name target normalizes to
the canonical id (case-insensitive, whole-graph set), an id passes
through untouched, an unknown target rejects one-line AND does not arm
the gate (honest activation — the next valid pose goes through), and
the legacy node-source-less wiring stays unvalidated. The base prompt
now states the convention: target takes the proposition ids from the
session context — ids are how problems are traced.
This commit is contained in:
amosblomqvist
2026-10-05 06:31:22 +03:00
parent 0dfb1de246
commit 83dad65481
2 changed files with 45 additions and 1 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ He has to be able to trust you completely; one confidently-delivered hallucinati
Two tools, and everything else is conversation. The mechanics only — the method (when to pose, what to pose, how to respond) is the doctrine's.
**`problem`** — poses one problem. The statement field is what the learner sees (markdown and LaTeX render); the modelSolution is your private commitment, hidden from the learner until you conclude. The call ends the turn. The learner then works in a thinking field; each submission arrives back as a *Your thinking* message. Whether it claims an answer is IN THE TEXT, not declared anywhere — read it:
**`problem`** — poses one problem. The statement field is what the learner sees (markdown and LaTeX render); the modelSolution is your private commitment, hidden from the learner until you conclude. The target field takes the proposition ids from the session context — ids are how every problem is traced to the propositions it exercises. The call ends the turn. The learner then works in a thinking field; each submission arrives back as a *Your thinking* message. Whether it claims an answer is IN THE TEXT, not declared anywhere — read it:
- Working trace (mid-work, stuck, or asking for a nudge): the hint ladder applies, one rung per request. A trace may span several submissions.
- A claimed answer: extract the moves, deliver the verdict. A claim is not a progress signal.
+44
View File
@@ -135,6 +135,50 @@ describe("the practice tool gating law", () => {
});
});
// ── target validation (the mastery-trace contract, owner round 9) ──────────
describe("target validation — names normalize to ids, unknowns reject", () => {
const NODES = [
{ id: "n_parity", name: "Parity of a sum", statement: "The sum of two odds is even." },
{ id: "n_pairing", name: "Pairing", statement: "Pair elements to count parity." },
] as const;
const graphNodes = () => NODES;
it("a NAME target silently normalizes to the canonical id (the record carries ids)", async () => {
const healed = problemTool(createPracticeGate(ctxOf([])), graphNodes).prepareArguments!({
...PROBLEM_INPUT,
target: ["Parity of a sum", "pairing"],
}) as { target: string[] };
assert.deepEqual(healed.target, ["n_parity", "n_pairing"], "case-insensitive name → id, whole-graph set");
});
it("an id target passes through untouched (already canonical)", async () => {
const healed = problemTool(createPracticeGate(ctxOf([])), graphNodes).prepareArguments!({
...PROBLEM_INPUT,
target: ["n_parity"],
}) as { target: string[] };
assert.deepEqual(healed.target, ["n_parity"]);
});
it("an UNKNOWN target rejects with a one-line error and does NOT arm the gate", async () => {
const gate = createPracticeGate(ctxOf([]));
const tool = problemTool(gate, graphNodes);
const rejected = await tool.execute("call_x", { ...PROBLEM_INPUT, target: ["ghost-prop"] } as never);
assert.equal(rejected.isError, true);
assert.match((rejected.details as { problems: string[] }).problems[0]!, /ghost-prop/);
assert.match((rejected.details as { problems: string[] }).problems[0]!, /proposition ids from the session context/);
// honest activation (the BKT law): a rejected pose frees the slot —
// the next, valid pose goes through.
const valid = await tool.execute("call_y", { ...PROBLEM_INPUT, target: ["n_parity"] } as never);
assert.equal(valid.isError, undefined);
});
it("NO node source (the legacy/test wiring) = no validation, free strings ack", async () => {
const result = await problemTool(createPracticeGate(ctxOf([]))).execute("call_x", PROBLEM_INPUT as never);
assert.equal(result.isError, undefined);
});
});
// ── the registry + the agent ────────────────────────────────────────────────
describe("the practice agent", () => {