Node schema pivot: short name (label) + full-precision statement; description dropped

With real extracted data, full-sentence statements as canvas labels proved
unworkable: 10-line label blocks, illegible overprint clusters, bottom-edge
clipping. Per the user's 2026-09-08 decision:

- GraphNode/ExtractedProposition gain `name` — a very short, glanceable
  label (glossary-headword style: "Will to power"), presentational only:
  not unique, not identity, never precise. Set at creation; link-appends
  never change it (first line defines; labels stay stable across sessions).
- `statement` keeps its full precision and becomes the identity anchor;
  the detail card shows it under the name. `description` is dropped.
- Extractor coins the name (freshest session context); auditor carries it
  through on create, rewording only clearly unglanceable names; linked
  propositions keep the existing node's name.
- Renderer: canvas label = name; search matches name + statement;
  detail card = name (bold) + statement (muted).
- Store guard requires name (pre-pivot node lines are skipped as legacy);
  vision.md node-schema bullet + amendments table updated (2026-09-08).
This commit is contained in:
amosblomqvist
2026-09-09 00:40:02 +03:00
parent 021c659d14
commit bdd9cac2b7
10 changed files with 101 additions and 72 deletions
+19 -11
View File
@@ -99,6 +99,13 @@ const messagesField = z
const propositionSchema = z.object({
id: z.string().describe("Local id: \"p1\", \"p2\", … — referenced by edges and attribution."),
name: z
.string()
.describe(
"A very short label for the proposition — a few words, a compact handle like a glossary headword " +
'(\'Will to power\', \'Divine command theory\'). Not a sentence, needn\'t be unique or ' +
"self-contained: the statement carries the precision. This becomes the node's label in the graph.",
),
statement: z
.string()
.describe(
@@ -107,11 +114,6 @@ const propositionSchema = z.object({
'"X". A misconception is stated as the TRUE correction ("Nietzsche did not endorse ' +
'might-makes-right social Darwinism") — the wrongness lives in the quiz outcomes, never here.',
),
description: z
.string()
.describe(
"The elaborated version of the same proposition — more words: scope, precision, what exactly it means. Still true, still world-facing.",
),
aliases: z.array(z.string()).describe("Verbatim phrasings this session used for the same proposition, if any."),
messages: messagesField,
evidence,
@@ -168,8 +170,12 @@ misconception is NOT a node of its own wrong claim — state the TRUE correction
quiz outcomes carry that the learner believed the wrong version.
1. PROPOSITIONS — every distinct reasoning-step-sized proposition the session touched:
- Each is a self-contained TRUE statement ("statement"), plus its elaboration ("description" — more
words, same proposition: scope, precision, what exactly it means).
- Each has a "name" — a VERY short label, a few words, a compact handle like a glossary headword
("Will to power", "Divine command theory"). Not a sentence; needn't be unique or self-contained.
It becomes the node's label in the graph, so keep it glanceable: names are what one reads on
the graph, statements are what one reads on the node.
- And a "statement" — the self-contained TRUE proposition at full precision (the elaboration lives
in the graph only as this one statement).
- Include corrections of misconceptions the teacher articulated, and true facts explained in prose.
- One reasoning step per proposition: never a topic blob, never a sub-step.
- "messages": the [id] tags of every message where the proposition came up — provenance, report it.
@@ -254,10 +260,12 @@ FAITHFULNESS — verify every candidate against the transcript:
scale or drop.
IDENTITY — link-or-create, per proposition:
- You see the current graph (every node: id, statement, description, aliases). For each verified proposition,
- You see the current graph (every node: id, name, statement, aliases). For each verified proposition,
decide: does an EXISTING node already state this same proposition?
- If yes: set "linkTo" to that node's id. The session's phrasings will join its aliases and its provenance.
- If no: omit "linkTo" and a new node is created.
- If yes: set "linkTo" to that node's id. The session's phrasings will join its aliases and its provenance;
the node keeps its existing name (labels stay stable across sessions — ignore your name for linked nodes).
- If no: omit "linkTo" and a new node is created, with the extractor's name as its label. Reword only a
clearly unglanceable name (an unwieldy sentence of a name); keep it short.
- When unsure, omit — duplicates are recoverable later; wrong merges are not.
Also drop propositions not referenced by anything else ONLY if they have no other backing — an unreferenced but
@@ -271,7 +279,7 @@ function graphView(nodes: GraphNode[]): string {
if (nodes.length === 0) return "(the graph is currently empty — every proposition is a new node)";
const lines = nodes.map(
(n) =>
`- ${n.id} :: ${n.statement}\n description: ${n.description}` +
`- ${n.id} :: ${n.name}\n statement: ${n.statement}` +
(n.aliases.length > 0 ? `\n aliases: ${n.aliases.join(" | ")}` : ""),
);
return lines.join("\n");
+22 -13
View File
@@ -33,13 +33,18 @@ export interface ProvenancePoint {
export interface GraphNode {
/** Opaque stable id, store-assigned: "n_" + nanoid-like. */
id: string;
/** The minimal proposition — one true, self-contained statement. This is
* BOTH the rendering label AND the identity key: the auditor recognizes
* a re-encounter by judgment (link-or-create), not by string matching. */
/** A very short label for the node — a compact handle like a glossary
* headword ("Will to power", "Divine command theory"): what the graph
* shows at a glance. Presentational only — not unique, not an identity
* key, never precise: the statement carries the precision. Set at
* creation (first line defines); a link-append never changes it. */
name: string;
/** The minimal proposition — one true, self-contained statement, at
* full precision. The IDENTITY ANCHOR: the auditor recognizes a
* re-encounter by judgment against this (link-or-create), not by
* string matching. Shown in the detail card; the canvas label is the
* name. */
statement: string;
/** The elaborated version — required. Same proposition, more words:
* scope, precision, the "what exactly does it mean". */
description: string;
/** Verbatim phrasings sessions used for this proposition (the identity
* audit trail — how the world said what the node says). */
aliases: string[];
@@ -108,10 +113,11 @@ export interface LedgerEvent {
* (nodes/edges/events/mappings/sessions), all append-only.
*
* nodes.jsonl one line per node — the FIRST line for an id defines
* statement/description/createdAt; later lines for the
* name/statement/createdAt; later lines for the
* same id are link-appends (a session's re-encounter):
* their statements/aliases join `aliases`, their
* provenance unions. getGraph merges by id.
* provenance unions; the name stays the first line's.
* getGraph merges by id.
* edges.jsonl one line per edge ASSERTION; readers merge by
* (from,to,type), unioning provenance.
*
@@ -136,13 +142,16 @@ export interface LedgerEvent {
export interface ExtractedProposition {
/** Extractor-local id ("p1", …) referenced by edges + attribution. */
id: string;
/** A very short label — a few words, a compact handle like a glossary
* headword. Not a sentence, needn't be unique or self-contained: the
* statement carries the precision. Becomes the node's name on create;
* on link, the existing node keeps its name. */
name: string;
/** The minimal proposition — a true, self-contained, world-facing
* statement. No learner / teacher / session references: misconceptions
* are stated as the true correction, and wrongness lives in the event
* ledger, never in the node. */
* statement, at full precision. No learner / teacher / session
* references: misconceptions are stated as the true correction, and
* wrongness lives in the event ledger, never in the node. */
statement: string;
/** The elaborated version — required. */
description: string;
/** Verbatim phrasings the session used. */
aliases: string[];
/** The message ids where this proposition came up — provenance. */
+7 -4
View File
@@ -166,10 +166,12 @@ export function mergeFindings(chatId: string, audit: AuditResult, deps: GraphDep
// line — the session's phrasings (statement + aliases) join the
// node's aliases, the provenance unions. Reads merge by id.
graphNodeId = p.linkTo;
// Link-append: phrasings + provenance join at read time (the
// first line's name/statement/createdAt stand).
ledgers.appendNode({
id: graphNodeId,
name: p.name,
statement: p.statement,
description: p.description,
aliases: [...p.aliases],
createdAt: now,
provenance: points,
@@ -177,8 +179,8 @@ export function mergeFindings(chatId: string, audit: AuditResult, deps: GraphDep
} else {
const node: GraphNode = {
id: newGraphNodeId(),
name: p.name,
statement: p.statement,
description: p.description,
aliases: [...p.aliases],
createdAt: now,
provenance: points,
@@ -329,8 +331,9 @@ export interface DerivedGraph {
/**
* The whole graph from the ledgers. Node LINES may repeat an id (link-
* appends): the FIRST line defines statement/description/createdAt; later
* lines' statements and aliases join `aliases`, provenance unions. Edge
* appends): the FIRST line defines name/statement/createdAt; later
* lines' statements and aliases join `aliases`, provenance unions — the
* name never changes after creation. Edge
* LINES are assertions, merged by (from,to,type) with unioned provenance,
* first-seen order. `store events, derive state` — this is the only reader
* that matters.
+1 -1
View File
@@ -83,8 +83,8 @@ const isProvenancePoint = (v: unknown): v is ProvenancePoint =>
const isGraphNode = (v: unknown): v is GraphNode =>
isRecord(v) &&
isString(v.id) &&
isString(v.name) &&
isString(v.statement) &&
isString(v.description) &&
Array.isArray(v.aliases) &&
v.aliases.every(isString) &&
isString(v.createdAt) &&