Graph nodes gain a name; nodes are objective facts (no learner/session references)

Schema: GraphNode + ExtractedProposition carry 'name' — a short 2-6 word
header, the rendering handle. Node theory made explicit: a node is an
objective world-facing fact; the learner's relationship to it lives
entirely in the event ledger (state labels are derived, never stored).
Extractor + auditor instructions enforce it — misconceptions state the
wrong idea itself as a claim, never 'the learner believes Y'. Legacy
ledger lines without a name derive one from the statement on read.
graph-view: labels use the name; the detail panel shows name as header
and statement as body.
This commit is contained in:
Amos
2026-09-01 03:34:47 +03:00
parent 49a6a6d9a0
commit 2e4e43b8c4
9 changed files with 84 additions and 14 deletions
+25 -2
View File
@@ -99,7 +99,18 @@ const findingsSchema = z.object({
.array(
z.object({
id: z.string().describe("Local id: \"p1\", \"p2\", … — referenced by edges and attribution."),
statement: z.string().describe("The proposition as one reasoning step — a node statement."),
name: z.string().describe(
"A short 2-6 word name for the fact — its header, used for rendering. Not a sentence.",
),
statement: z
.string()
.describe(
"The proposition as one reasoning step — an OBJECTIVE, world-facing fact. " +
"Never reference the learner, the teacher, or the session: not \"the learner " +
'understands X\" but \"X\"; not \"the learner thinks Y\" but the wrong idea itself, ' +
'stated as a claim (\"Reading: Nietzsche endorsed might-makes-right social Darwinism\"), ' +
"with kind \"misconception\" carrying that it is wrong.",
),
aliases: z.array(z.string()).describe("Verbatim alternate phrasings from this session, if any."),
kind: z.enum(["truth", "misconception"]).describe("misconception = a wrong idea the learner holds or was warned about."),
treeNodeId: z.string().optional().describe("The plan-tree node id this came from, when it did."),
@@ -134,7 +145,17 @@ verbatim (LEARNER / TEACHER), and structured events (QUIZ lines with the learner
Report, with verbatim evidence quotes for everything:
NODE THEORY — the graph is a set of objective facts, full stop. A node is what is true (or falsely
believed) about the world, never anything about the learner or the session. The learner's
relationship to a fact — whether they know it, missed it, or hold it as a misconception — is
expressed ENTIRELY by the quiz-outcome events you attribute; it never leaks into the node itself.
So no "the learner understands X", no "discussed in this session": the node is just X. A
misconception node states the wrong idea itself as a claim, with kind "misconception" marking it
wrong — never "the learner believes Y".
1. PROPOSITIONS — every distinct reasoning-step-sized proposition the session touched:
- Each carries a "name": a short 2-6 word header (the rendering handle), and a "statement": the
full fact as one objective reasoning step.
- Plan-tree nodes become propositions (pass their treeNodeId) — statement from the node title, enriched to a
complete proposition if the prose elaborates it.
- Propositions that appeared only in prose (teacher explanations, learner questions) get fresh ids.
@@ -201,7 +222,9 @@ verified subset.
Verify:
- PROPOSITIONS: every evidence quote must appear (near-)verbatim in the transcript view, and the statement must
be a faithful reading of it. Drop invented or bloated statements. Conservative: when two proposed propositions
be a faithful reading of it. The node theory applies: a statement must be an OBJECTIVE, world-facing fact —
reword learner- or session-relative statements ("the learner understands X" → "X") or drop them when the
objective fact is not actually backed. Drop invented or bloated statements. Conservative: when two proposed propositions
are really one, keep the better-stated one and drop the other.
- EDGES: the evidence must actually articulate the relation, in the direction claimed (from = prerequisite).
Drop direction-swapped or speculative edges.
+14 -2
View File
@@ -66,7 +66,15 @@ export type EdgeType = "requires" | "supports";
export interface GraphNode {
/** Opaque stable id, store-assigned: "n_" + nanoid-like. */
id: string;
/** Canonical proposition statement. */
/** Short name / header for the fact (2-6 words) — the rendering handle.
* NODE THEORY: a node is an OBJECTIVE fact, never learner- or
* session-relative. No "the learner understands…", no "this session
* covered…" — just the fact, world-facing. The learner's relationship
* to the fact lives ENTIRELY in the event ledger: state labels are
* derived from quiz outcomes, never written into the node. */
name: string;
/** Canonical proposition statement — the full fact, one reasoning step,
* also objective (no learner / teacher / session references). */
statement: string;
/** Verbatim alternate statements from sessions (merge target audit trail). */
aliases: string[];
@@ -146,7 +154,11 @@ export interface LedgerEvent {
export interface ExtractedProposition {
/** Extractor-local id ("p1", …) referenced by edges + attribution. */
id: string;
/** Canonical one-reasoning-step statement. */
/** Short name / header for the fact (2-6 words) — the rendering handle. */
name: string;
/** Canonical one-reasoning-step statement — an objective, world-facing
* fact (no learner / teacher / session references; the learner's
* relationship to it is carried only by the event attribution). */
statement: string;
/** Verbatim alternate phrasings from the session (merge aliases). */
aliases: string[];
+1
View File
@@ -160,6 +160,7 @@ export function mergeFindings(chatId: string, findings: SessionFindings, deps: G
} else {
const graphNode: GraphNode = {
id: newGraphNodeId(),
name: p.name,
statement: p.statement,
aliases: [...p.aliases],
kind: p.kind,
+19 -4
View File
@@ -69,13 +69,24 @@ function readValid<T>(file: string, isValid: (v: unknown) => v is T): T[] {
return readRawLines(file).filter(isValid);
}
const isGraphNode = (v: unknown): v is GraphNode =>
/** Pre-name ledger line (the first extraction runs) — read tolerantly. */
type StoredGraphNode = Omit<GraphNode, "name"> & { name?: string };
const isStoredGraphNode = (v: unknown): v is StoredGraphNode =>
isRecord(v) &&
isString(v.id) &&
isString(v.statement) &&
Array.isArray(v.aliases) &&
(v.kind === "truth" || v.kind === "misconception") &&
isString(v.createdAt);
isString(v.createdAt) &&
(v.name === undefined || isString(v.name));
/** Derived header for legacy node lines written before `name` existed:
* the first clause of the statement, capped — good enough to render. */
function nameFromStatement(statement: string): string {
const clause = statement.split(/[.;:—]/)[0]!.trim();
return clause.length > 60 ? `${clause.slice(0, 57)}…` : clause || statement;
}
const EDGE_TYPES = ["requires", "supports"] as const;
@@ -116,9 +127,13 @@ export const graphStore: GraphStore = {
appendNode(node: GraphNode): void {
appendLine(LEDGER_FILES.nodes, node);
},
/** Raw node lines in append order (ids are unique — no merging needed). */
/** Raw node lines in append order (ids are unique — no merging needed).
* Legacy lines without a `name` get one derived from the statement. */
readNodes(): GraphNode[] {
return readValid(LEDGER_FILES.nodes, isGraphNode);
return readValid(LEDGER_FILES.nodes, isStoredGraphNode).map((n) => ({
...n,
name: n.name ?? nameFromStatement(n.statement),
}));
},
/** Append one edge ASSERTION; readers merge lines by (from,to,type). */