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
+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). */