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:
+16
-15
@@ -10,21 +10,22 @@
|
||||
* and cancelled on unmount — idle CPU stays near zero. Physics is
|
||||
* placement only: pan/zoom, no dragging, no editing.
|
||||
*
|
||||
* Labels render the node `statement` (the minimal proposition — identity
|
||||
* key + rendering label; the full `description` lives on the detail card)
|
||||
* through custom canvas drawers that replace sigma's defaults:
|
||||
* Labels render the node `name` — the very short, glanceable label the
|
||||
* extractor coins (a glossary-headword-style handle); the full-precision
|
||||
* `statement` lives on the detail card. Custom canvas drawers replace
|
||||
* sigma's defaults:
|
||||
* - defaultDrawNodeLabel: plain wrapped text, centered below the node,
|
||||
* word-wrapped at a max line width — no truncation, no one-line sprawl.
|
||||
* - defaultDrawNodeHover: NO background box (sigma's default is a
|
||||
* hardcoded white one). Instead the hovered node gets a soft glow
|
||||
* halo in its own state color at a constant radius, flush with the
|
||||
* disc edge, and its statement brightens — the "this is clickable,
|
||||
* disc edge, and its name brightens — the "this is clickable,
|
||||
* click for the full fact" affordance. Entering/leaving hover fades
|
||||
* ONE transition factor shared by halo and text alike, so the
|
||||
* halo's opacity and the text's muted→bright cross-fade stay in
|
||||
* sync (no growth, no pulse; interpolating from the current value
|
||||
* means rapid hover toggles glide instead of snapping). Hover
|
||||
* always shows the statement, even when labels are zoomed out.
|
||||
* always shows the name, even when labels are zoomed out.
|
||||
* Node hit-testing is the disc PLUS a pad: sigma picks nodes from a
|
||||
* half-resolution framebuffer (no v3 setting enlarges hitboxes), and the
|
||||
* glow halo is bigger than the stock hitbox — touching the halo
|
||||
@@ -82,11 +83,11 @@ const STATES_ORDER: NodeStateClass[] = ["taught", "known", "missed", "dont-know"
|
||||
|
||||
/**
|
||||
* Pure filter predicate for the legend/filters panel: a node is visible
|
||||
* when its state is toggled on AND (no query OR statement matches).
|
||||
* when its state is toggled on AND (no query OR name/statement matches).
|
||||
* Exported for tests; the sigma reducers just consume its verdict.
|
||||
*/
|
||||
export function isNodeVisible(
|
||||
node: Pick<GraphNode, "statement" | "description">,
|
||||
node: Pick<GraphNode, "name" | "statement">,
|
||||
state: NodeStateClass,
|
||||
hiddenStates: ReadonlySet<NodeStateClass>,
|
||||
query: string,
|
||||
@@ -94,7 +95,7 @@ export function isNodeVisible(
|
||||
if (hiddenStates.has(state)) return false;
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
return node.statement.toLowerCase().includes(q) || node.description.toLowerCase().includes(q);
|
||||
return node.name.toLowerCase().includes(q) || node.statement.toLowerCase().includes(q);
|
||||
}
|
||||
|
||||
// ---- Label drawing (canvas) ---------------------------------------------
|
||||
@@ -102,7 +103,7 @@ export function isNodeVisible(
|
||||
// sigma's defaults draw the label on ONE line to the right of the node with
|
||||
// a white box on hover. We replace both: names wrap under the node at a max
|
||||
// line width, muted at rest; on hover the node pulses a soft glow in its
|
||||
// own state color and the statement brightens (no background box).
|
||||
// own state color and the name brightens (no background box).
|
||||
|
||||
const LABEL_MAX_WIDTH = 170; // px — wraps long names instead of a wide strip
|
||||
const LABEL_GAP = 6; // px between node edge and first label line
|
||||
@@ -227,7 +228,7 @@ function wrapLabel(context: CanvasRenderingContext2D, text: string, maxWidth: nu
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The wrapped node statement, centered below the node — used by both drawers
|
||||
/** The wrapped node name (the short label), centered below the node — used by both drawers
|
||||
* with identical geometry so hover only changes the color, nothing jumps. */
|
||||
function drawName(
|
||||
context: CanvasRenderingContext2D,
|
||||
@@ -254,7 +255,7 @@ function drawName(
|
||||
* 1 = fully hovered): a glow halo in the node's own state color at a
|
||||
* CONSTANT radius (opacity fades in/out; the halo never grows) starting
|
||||
* flush at the disc edge (the evenodd punch-out keeps the disc itself
|
||||
* unpainted). The statement is drawn at partial alpha in the bright hover
|
||||
* unpainted). The name is drawn at partial alpha in the bright hover
|
||||
* color — the muted resting label on the layer beneath shows through,
|
||||
* so text and halo cross-fade on the SAME factor and stay in sync. */
|
||||
function paintHover(
|
||||
@@ -278,7 +279,7 @@ function paintHover(
|
||||
context.fill("evenodd");
|
||||
}
|
||||
// Same fade factor as the halo: text and glow rise together. The
|
||||
// bright statement at partial alpha over the muted resting label beneath is
|
||||
// bright name at partial alpha over the muted resting label beneath is
|
||||
// a smooth muted→bright cross-fade rather than an instant color swap.
|
||||
context.globalAlpha = fade;
|
||||
drawName(context, data, settings, hoverTextColor);
|
||||
@@ -435,7 +436,7 @@ export function GraphView() {
|
||||
// frozen layout stable for a given node set across remounts.
|
||||
const rand = seededRandom(node.id);
|
||||
graph.addNode(node.id, {
|
||||
label: node.statement,
|
||||
label: node.name,
|
||||
size: STATE_TOKENS[state].size,
|
||||
color: stateColors.get(state) ?? edgeColor,
|
||||
x: (rand() - 0.5) * 100,
|
||||
@@ -695,8 +696,8 @@ export function GraphView() {
|
||||
style={{ visibility: "hidden", left: 0, top: 0 }}
|
||||
className="absolute z-10 w-80 max-w-[calc(100%-1rem)] rounded-xl border bg-card p-4 text-left shadow-lg"
|
||||
>
|
||||
<div className="text-sm font-medium leading-snug">{selected.node.statement}</div>
|
||||
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">{selected.node.description}</div>
|
||||
<div className="text-sm font-medium leading-snug">{selected.node.name}</div>
|
||||
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">{selected.node.statement}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-wide text-muted-foreground">
|
||||
{STATE_LABELS[selected.state]}
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -21,7 +21,7 @@ The graph updates **continuously, not per-session**. A learner who answers a few
|
||||
The global graph is an **objective world-model of the domains the learner has encountered**: the facts, their structure (what follows from / is supported by what), and — inferred from that structure plus the event ledger — the state of the learner's understanding. Nothing learner- or session-relative is ever stored **in** the graph; understanding is **inferred, never encoded**.
|
||||
|
||||
- **Every node is a true proposition.** Worded objectively, in minimal form as it exists w.r.t. its context. Truth is context-relative and that is fine: "Gravity is a force" and "Gravity is not a force" coexist as nodes — each true in its context (classical mechanics vs. GR), with **the surrounding subgraph as the context**. No context field, no pedantic self-containment of facts.
|
||||
- **Node schema:** `statement` — the proposition in minimal form (a short sentence; the rendering label and identity key); `description` — the elaborated version (required); `aliases` — the verbatim phrasings sessions used (merge keys + audit trail). There is **no `kind`** field.
|
||||
- **Node schema:** `name` — 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. Set at creation; a re-encounter never changes it. `statement` — the proposition at full precision (the identity anchor; shown in the detail card). `aliases` — the verbatim phrasings sessions used (merge keys + audit trail). There is **no `kind`** and **no `description`** (2026-09-08 amendment: with real data, full-sentence canvas labels proved unworkable — ten-line labels, illegible overprint. The label is the name; the precision lives one interaction away in the statement).
|
||||
- **Misconceptions are not a node category.** A myth enters the graph as its true correction ("The 10%-of-brain claim is a myth"); the learner's holding of the wrong idea lives entirely in the event ledger. Wrongness is about the learner, and the learner lives in the ledger.
|
||||
|
||||
### Edges (DECIDED 2026-09-02)
|
||||
@@ -226,6 +226,7 @@ The teacher's context window and attention are precious — it has the lesson to
|
||||
| `taught` events (marking things done) | Dropped; the extractor records per-proposition understanding notes (named-word scale) as session-scoped observations instead |
|
||||
| Chat-granular node/edge provenance | Message-granular (chat, message) provenance on nodes and edges — the exact derivation points |
|
||||
| `name` + `statement` on nodes | `statement` (minimal proposition, the rendering label) + `description` (required elaboration) + `aliases` (verbatim phrasings) |
|
||||
| `statement` + `description` (2026-09-02) | `name` (very short label, the rendering handle) + `statement` (full precision, identity anchor, detail card); `description` dropped — full-sentence canvas labels cluttered the graph (2026-09-08, seeing real extracted data) |
|
||||
| Updates per completed session | Continuous; partial sessions deposit value |
|
||||
| Mermaid DAG as the plan artifact | Structured tools as source of truth; mermaid only for ad-hoc diagrams |
|
||||
| UI deferred, "not important" | UI exists (Next.js, v3.4); the graph view is the app's home page |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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) &&
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
@@ -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 }],
|
||||
|
||||
Reference in New Issue
Block a user