Author-once plan protocol: single mermaid plan_init + derived statuses + live quiz nodeId validation
- plan_init is now the ONLY plan tool: a mermaid 'graph TD' block parsed by a constrained-subset parser (mermaid-plan.ts) with line-precise errors; the seven mutation/tracking tools are gone by design (2026-09-03 design pass). - Re-call plan_init = sanctioned wholesale rewrite; slugs are permanent. - derivePlanStatuses (contracts/plan.ts): done = passed check tagged with a quiz nodeId; current = open check's node else first topo-ready node — the tracking the teacher no longer declares by hand. - questionToolsFor(ctx) (tools.ts): the quiz schema chains a dynamic plan- nodeId validation against the LIVE record — an invalid nodeId is rejected at the call boundary and the teacher retries mid-turn, before any UI. Static questionTools stay validator-less for the default path. - Prompts rewritten for mermaid authoring + derived position. - Out-of-lane surgical touches (flagged for master review): src/core/question-core.ts (quizInputObject export + nodeId description), src/core/contracts/plan.ts (derivePlanStatuses + doc).
This commit is contained in:
@@ -44,14 +44,85 @@ export function planNodeStatus(tree: PlanTree, nodeId: string): PlanNodeStatus {
|
||||
return "upcoming";
|
||||
}
|
||||
|
||||
// ─── derived statuses: the author-once protocol (2026-09-03) ─────────────────────
|
||||
//
|
||||
// The teacher AUTHORS the map (plan_init, mermaid — once at the seam,
|
||||
// re-callable to rewrite) and never tracks progress: statuses derive from
|
||||
// the transcript, pure and at render time. A check names its node (quiz
|
||||
// input nodeId — validated at the call boundary against the live tree);
|
||||
// the derivation reads outcomes:
|
||||
// done — any check on the node that the learner PASSED (demonstration-
|
||||
// only: "explaining is yours, done is theirs")
|
||||
// current — the node of the last UNANSWERED check; if none, the first
|
||||
// node in authoring order whose prerequisites are all done and
|
||||
// that isn't done itself (forward-looking: the panel leads the
|
||||
// lesson — during teaching it points at what's coming, and holds
|
||||
// through the check that follows)
|
||||
// upcoming — everything else
|
||||
// Untagged checks and nodeIds missing from the current tree are lost signal
|
||||
// (the standing doctrine); a rewritten map keeps honest progress for reused
|
||||
// slugs because the derivation matches ids against the CURRENT tree.
|
||||
|
||||
/** Minimal structural message slice — anything with typed parts (mirrors
|
||||
* phases.ts: pure, no runtime deps, safe on server, client, and in tests). */
|
||||
interface PartsCarrier {
|
||||
parts: Array<{ type: string; state?: string; input?: unknown; output?: unknown }>;
|
||||
}
|
||||
|
||||
export interface DerivedPlanStatuses {
|
||||
current: string | null;
|
||||
/** Node ids demonstrated by the learner (passed checks). */
|
||||
done: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
/** Statuses of a node: the derived view the plan panel should render. */
|
||||
export function derivedNodeStatus(statuses: DerivedPlanStatuses, nodeId: string): PlanNodeStatus {
|
||||
if (statuses.current === nodeId) return "current";
|
||||
if (statuses.done.has(nodeId)) return "done";
|
||||
return "upcoming";
|
||||
}
|
||||
|
||||
export function derivePlanStatuses(tree: PlanTree, messages: readonly PartsCarrier[]): DerivedPlanStatuses {
|
||||
const ids = new Set(tree.nodes.map((n) => n.id));
|
||||
const done = new Set<string>();
|
||||
let current: string | null = null;
|
||||
|
||||
for (const m of messages) {
|
||||
for (const part of m.parts) {
|
||||
if (part.type !== "tool-quiz") continue;
|
||||
const nodeId = (part.input as { nodeId?: unknown } | undefined)?.nodeId;
|
||||
if (typeof nodeId !== "string" || !ids.has(nodeId)) continue; // untagged/unknown = lost signal
|
||||
if (part.state === "input-available") {
|
||||
current = nodeId; // a check is out — that's where we are
|
||||
} else if (part.state === "output-available") {
|
||||
if ((part.output as { correct?: unknown } | undefined)?.correct === true) done.add(nodeId);
|
||||
// failed checks change nothing: resume teaching, re-check later
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current === null) {
|
||||
// No check out: the first node (authoring order) whose prerequisites
|
||||
// are all demonstrated and that isn't done itself — the next lesson.
|
||||
const prereqs = new Map<string, string[]>();
|
||||
for (const e of tree.edges) prereqs.set(e.to, [...(prereqs.get(e.to) ?? []), e.from]);
|
||||
current =
|
||||
tree.nodes.find(
|
||||
(n) => !done.has(n.id) && (prereqs.get(n.id) ?? []).every((p) => done.has(p)),
|
||||
)?.id ?? null;
|
||||
}
|
||||
return { current, done };
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan tools (W1 implements; server-side WITH execute, mutate the chat's
|
||||
* stored tree, and return the full updated tree so the panel can render the
|
||||
* latest state straight from the stream). Every tool returns PlanToolResult.
|
||||
* Tool names: plan_init, plan_add_node, plan_update_node, plan_remove_node,
|
||||
* plan_add_edge, plan_remove_edge, plan_set_current, plan_mark_done.
|
||||
* The tree panel derives its display from the LATEST plan tool output in the
|
||||
* transcript (live during streaming) — no separate state channel.
|
||||
* The plan tool result (author-once protocol, 2026-09-03): ONE server-side
|
||||
* tool, plan_init (tools/plan.ts) — a mermaid `graph TD` block canonicalized
|
||||
* into the structured tree, called once at the probe→teach seam and
|
||||
* re-callable to rewrite. The former mutation/tracking tools are gone:
|
||||
* statuses derive from check outcomes (derivePlanStatuses above), so the
|
||||
* tree carries structure, never progress. The tree panel still derives its
|
||||
* display from the LATEST plan tool output in the transcript — plus the
|
||||
* derived statuses — with no separate state channel.
|
||||
*/
|
||||
export interface PlanToolResult {
|
||||
ok: boolean;
|
||||
|
||||
Reference in New Issue
Block a user