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
+4 -4
View File
@@ -135,8 +135,8 @@ const FINDINGS: SessionFindings = {
propositions: [
{
id: "p1",
name: "Derivative",
statement: "The derivative measures how fast a function changes",
description: "The derivative of a function at a point is its instantaneous rate of change there.",
aliases: [],
messages: ["a2"],
evidence: ["TEACHER [a2]: The derivative measures how fast a function changes."],
@@ -144,8 +144,8 @@ const FINDINGS: SessionFindings = {
},
{
id: "p2",
statement: "Chain rule intuition",
description: "The chain rule differentiates composed functions by multiplying local rates.",
name: "Chain rule",
statement: "The chain rule differentiates composed functions by multiplying local rates.",
aliases: [],
messages: ["a2"],
evidence: ["LEARNER [u1]: teach me the chain rule"],
@@ -267,8 +267,8 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
const existingId = "n_existing";
ledgers.appendNode({
id: existingId,
name: "Derivative",
statement: "Derivatives measure rates of change",
description: "Pre-existing node from an earlier session.",
aliases: [],
createdAt: "2026-08-01T00:00:00.000Z",
provenance: [],
+7 -7
View File
@@ -51,8 +51,8 @@ import { PATHS } from "../config.ts";
function node(id: string, statement: string, over: Partial<GraphNode> = {}): GraphNode {
return {
id,
name: statement,
statement,
description: `${statement} — elaborated.`,
aliases: [],
createdAt: "2026-08-22T00:00:00.000Z",
provenance: [],
@@ -346,16 +346,16 @@ describe("cross-chat edge assertions (merger + read-time merging)", () => {
propositions: [
{
id: "p1",
statement: "Derivative as a limit",
description: "The derivative is the limit of difference quotients.",
name: "Derivative as a limit",
statement: "The derivative is the limit of difference quotients.",
aliases: [],
messages: ["u1"],
evidence: ["e"],
},
{
id: "p2",
statement: "Chain rule intuition",
description: "Composed functions differentiate by multiplying local rates.",
name: "Chain rule",
statement: "Composed functions differentiate by multiplying local rates.",
aliases: [],
messages: ["u1"],
evidence: ["e"],
@@ -377,8 +377,8 @@ describe("cross-chat edge assertions (merger + read-time merging)", () => {
propositions: [
{
id: "q1",
name: "Derivative as a limit",
statement: "The derivative as a limit of quotients", // different phrasing
description: "…",
aliases: [],
messages: ["u1"],
evidence: ["e"],
@@ -386,8 +386,8 @@ describe("cross-chat edge assertions (merger + read-time merging)", () => {
},
{
id: "q2",
name: "Chain rule",
statement: "Chain rule, intuitively",
description: "…",
aliases: [],
messages: ["u1"],
evidence: ["e"],
+9 -4
View File
@@ -10,8 +10,8 @@ import type { NodeStateClass } from "../src/core/index.ts";
const none: ReadonlySet<NodeStateClass> = new Set();
const node = {
statement: "The money multiplier is the maximum ratio of broad money to base money.",
description: "How much broad money banks create from each unit of base money.",
name: "Money multiplier",
statement: "The maximum ratio of broad money a bank can create from each unit of base money.",
};
describe("isNodeVisible (legend/filters predicate)", () => {
@@ -33,11 +33,16 @@ describe("isNodeVisible (legend/filters predicate)", () => {
assert.equal(isNodeVisible(node, "candidate", none, "MONEY multiplier"), true);
});
it("search matches the description when the statement does not", () => {
assert.equal(isNodeVisible(node, "candidate", none, "maximum ratio"), true);
it("search matches the statement when the name does not", () => {
assert.equal(isNodeVisible(node, "candidate", none, "MAXIMUM ratio"), true);
assert.equal(isNodeVisible(node, "candidate", none, " velocity"), false);
});
it("search matches the name when the statement does not", () => {
assert.equal(isNodeVisible(node, "candidate", none, "money MULTIPLIER"), true);
assert.equal(isNodeVisible(node, "candidate", none, " banking"), false);
});
it("whitespace-only query is no filter", () => {
assert.equal(isNodeVisible(node, "missed", none, " "), true);
});
+14 -12
View File
@@ -167,8 +167,8 @@ function fixtureAudit(): AuditResult {
propositions: [
{
id: "p1",
name: "Alpha",
statement: "Alpha intuition",
description: "Alpha intuition, elaborated: what alpha means and how it behaves in practice.",
aliases: ["alpha feel"],
messages: ["a1", "a3"],
evidence: ["fixture evidence"],
@@ -176,16 +176,16 @@ function fixtureAudit(): AuditResult {
},
{
id: "p2",
name: "Beta rule",
statement: "Beta rule",
description: "The beta rule and when it applies.",
aliases: [],
messages: ["a4"],
evidence: ["fixture evidence"],
},
{
id: "p3",
name: "Gamma",
statement: "Gamma application",
description: "Applying the whole framework to gamma cases.",
aliases: [],
messages: ["a5"],
evidence: ["fixture evidence"],
@@ -241,7 +241,7 @@ describe("the merger (mergeFindings)", () => {
const idByStatement = new Map(graph.nodes.map((n) => [n.statement, n.id]));
for (const node of graph.nodes) {
assert.match(node.id, /^n_/);
assert.ok(node.description.length > 0, "description is required");
assert.ok(node.name.length > 0, "name is required");
assert.equal("kind" in node, false, "no kind field");
assert.ok(Array.isArray(node.provenance), "message-granular provenance");
}
@@ -333,8 +333,8 @@ describe("the merger (mergeFindings)", () => {
propositions: [
{
id: "q1",
name: "Alpha",
statement: "Alpha feel", // a DIFFERENT phrasing — identity is the verdict, not strings
description: "The intuitive grasp of alpha.",
aliases: ["alpha intuition"],
messages: ["a1"],
evidence: ["fixture evidence"],
@@ -390,8 +390,8 @@ describe("the merger (mergeFindings)", () => {
propositions: [
{
id: "r1",
name: "Alpha",
statement: "Alpha intuition", // IDENTICAL to the existing node's statement
description: "Same words, no verdict.",
aliases: [],
messages: ["u1"],
evidence: ["fixture evidence"],
@@ -473,22 +473,24 @@ describe("graph ledger store", () => {
it("round-trips lines and skips corrupt / foreign-shaped lines (strict reads)", () => {
const node = {
id: "n_storetest",
statement: "Store test",
description: "Store test, elaborated.",
name: "Store test",
statement: "Store test, elaborated.",
aliases: [] as string[],
createdAt: "2026-08-22T00:00:00.000Z",
provenance: [{ chatId: "chat_c", messageId: "m1" }],
};
graphStore.appendNode(node);
// A torn append and a legacy pre-redesign node (old schema) — both skipped.
// A torn append and a pre-name-pivot node (old schema: description, no
// name) — both skipped by the strict guard.
fs.appendFileSync(
LEDGER_FILES.nodes,
`{not json}\n${JSON.stringify({ id: "legacy", statement: "Legacy shape", kind: "truth", name: "Legacy", createdAt: "2026-08-22T00:00:00.000Z" })}\n`,
`{not json}\n${JSON.stringify({ id: "legacy", statement: "Legacy shape", description: "Old schema.", aliases: [], createdAt: "2026-08-22T00:00:00.000Z", provenance: [] })}\n`,
"utf-8",
);
const nodes = graphStore.readNodes().filter((n) => n.id === "n_storetest" || n.id === "legacy");
assert.equal(nodes.length, 1);
assert.equal(nodes[0]!.statement, "Store test");
assert.equal(nodes[0]!.name, "Store test");
assert.equal(nodes[0]!.statement, "Store test, elaborated.");
assert.deepEqual(nodes[0]!.provenance, [{ chatId: "chat_c", messageId: "m1" }]);
});
@@ -496,8 +498,8 @@ describe("graph ledger store", () => {
const line = (msgId: string) =>
JSON.stringify({
id: "n_linktest",
name: "Link test",
statement: `Phrasing ${msgId}`,
description: "Elaboration.",
aliases: [],
createdAt: "2026-08-22T00:00:00.000Z",
provenance: [{ chatId: "chat_c", messageId: msgId }],