extraction cleanup: events ledger dies, markdown source view, auditor-as-linker (2026-09-27 rulings)

The graph becomes raw reasoning chains only. Quiz outcomes and
understanding observations are no longer formalized; assessment
moves to the future practice module.

Events ledger removed end-to-end: LedgerEvent/LedgerEventKind/
UnderstandingLevel/ExtractionAttribution/AuditResult.dropped and
snapshot nodeObservations deleted from contracts; quizEvent builder
and the events step of mergeFindings gone; appendEvent/readEvents
dropped from the port and store; LEDGER_FILES loses events.jsonl
with a one-time purge of a stale file next to the nodes ledger;
graph-deletion and the delete dialog lose event purge + quiz-answer
impact counts; GraphSessionProposition becomes a pure birth list.

Whole-session markdown source view: new core/session-markdown.ts is
the shared formatting engine (id-anchored speaker blocks, quiz/
recall/question blocks, tool one-liners) — the extractor's prompt
view and the future user-facing export render from one engine.
Coverage fingerprints deliberately stay on the old line format
(content-hash semantics; re-formatting never re-opens covered
history). The whole session goes to the extractor — no phase cut.

Auditor refocused as the identity linker: never sees the source;
explores the graph via new auditor-specific tools in
agent/tools/graph.ts (find — glob over name/statement/aliases;
list_sessions; list_propositions with full statement text; no
timestamps) instead of an injected full-graph dump; equivalence
bias — paraphrases merge, only genuinely distinct propositions stay
apart; no drop channel, the extractor is fully trusted; may draw
bridging edges (typed follows/supports) between existing nodes and
the batch, resolved by the merger with chat-scoped message-less
provenance. Agentic loop, maxTurns 16.

UI: the node detail card gains an edges section above the sessions
list (follows-from / supported-by / what-follows / what-it-supports,
rendered only when present, click-to-select neighbors); the
understanding-note chips are gone.

Prober list_propositions reworded to what a session covered —
names only, no levels. docs/architecture.md + README updated to
the as-built pipeline. Gates: typecheck, 1059/1059, lint, build.
This commit is contained in:
amosblomqvist
2026-09-28 16:54:44 +03:00
parent cd09c12b56
commit 1b50013551
22 changed files with 909 additions and 1064 deletions
+8 -32
View File
@@ -11,7 +11,7 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import type { GraphEdge, GraphNode, LedgerEvent, NotesSource, ChatSummary } from "../src/core/contracts.ts";
import type { GraphEdge, GraphNode, NotesSource, ChatSummary } from "../src/core/contracts.ts";
import type { ChatStore, GraphStore } from "../src/core/ports.ts";
import {
graphSessionIndex,
@@ -36,7 +36,6 @@ const node = (id: string, name: string, provenance: Array<{ chatId: string; mess
function fakeStores(
nodes: GraphNode[],
over: {
events?: LedgerEvent[];
chats?: Array<Pick<ChatSummary, "id" | "title" | "createdAt" | "updatedAt">>;
sources?: Array<Pick<NotesSource, "id" | "filePath" | "importedAt">>;
mappings?: Array<{ chatId: string; sourceId: string; graphNodeId: string }>;
@@ -46,7 +45,6 @@ function fakeStores(
const graphStore: GraphStore = {
readNodes: () => nodes,
readEdges: () => edges,
readEvents: () => over.events ?? [],
readMappings: () => over.mappings ?? [],
readCheckpoints: () => [],
readCoverage: () => [],
@@ -160,12 +158,8 @@ describe("the notes mapping-line fallback", () => {
], "the file births under its src id; the childless chat has no entry (birth sessions only)");
});
it("graphSessionPropositions lists the file's births under the src id", () => {
const events: LedgerEvent[] = [
{ nodeId: "a", kind: "observation", chatId: "src_abc", at: "2026-09-22T00:00:00.000Z", note: "solid", ref: "obs:p1" },
];
it("graphSessionPropositions lists the file's births under the src id (a pure birth list, 2026-09-27)", () => {
const deps = fakeStores([node("a", "Alpha", []), node("b", "Beta", [])], {
events,
mappings: [
{ chatId: "src_abc", sourceId: "p1", graphNodeId: "a" },
{ chatId: "src_abc", sourceId: "p2", graphNodeId: "b" },
@@ -173,8 +167,8 @@ describe("the notes mapping-line fallback", () => {
sources: [{ id: "src_abc", filePath: "/vault/learn vim.md", importedAt: "2026-09-22T00:00:00.000Z" }],
});
assert.deepEqual(graphSessionPropositions(deps, "src_abc"), [
{ nodeId: "a", name: "Alpha", understanding: "solid", evaluatedAt: "2026-09-22T00:00:00.000Z" },
{ nodeId: "b", name: "Beta", understanding: null, evaluatedAt: null },
{ nodeId: "a", name: "Alpha" },
{ nodeId: "b", name: "Beta" },
]);
});
});
@@ -272,34 +266,16 @@ describe("graphSessionIndex (the prober's session list)", () => {
});
describe("graphSessionPropositions (the per-session view)", () => {
it("birth propositions with the latest extractor evaluation AND that event's own timestamp", () => {
it("birth propositions — a pure list, no evaluations (2026-09-27 cleanup)", () => {
const nodes = [
node("a", "Alpha", [pt("chat1")]),
node("b", "Beta", [pt("chat1")]),
node("c", "Gamma", [pt("chat2")]), // born elsewhere — never listed under chat1
];
const events: LedgerEvent[] = [
{ nodeId: "a", kind: "observation", chatId: "chat1", at: "2026-09-20T00:00:00.000Z", note: "shaky", ref: "obs:p1" },
// A LATER evaluation supersedes the earlier one for (a, chat1).
{ nodeId: "a", kind: "observation", chatId: "chat1", at: "2026-09-21T00:00:00.000Z", note: "solid", ref: "obs:p2" },
// Quiz evidence (owner ruling 2026-09-23): recorded in the ledger
// but it NEVER moves the evaluation date.
{ nodeId: "a", kind: "correct", chatId: "chat1", at: "2026-09-22T00:00:00.000Z", note: "got it" },
// Another session's evaluation of the same node is not ours.
{ nodeId: "a", kind: "observation", chatId: "chat2", at: "2026-09-23T00:00:00.000Z", note: "resisted" },
// Same-at events: ledger order breaks the tie (the snapshot's rule).
{ nodeId: "b", kind: "observation", chatId: "chat1", at: "2026-09-20T00:00:00.000Z", note: "accepted", ref: "obs:p3" },
{ nodeId: "b", kind: "observation", chatId: "chat1", at: "2026-09-20T00:00:00.000Z", note: "not yet engaged", ref: "obs:p4" },
];
const deps = fakeStores(nodes, { events });
const deps = fakeStores(nodes);
assert.deepEqual(graphSessionPropositions(deps, "chat1"), [
{ nodeId: "a", name: "Alpha", understanding: "solid", evaluatedAt: "2026-09-21T00:00:00.000Z" },
{ nodeId: "b", name: "Beta", understanding: "not yet engaged", evaluatedAt: "2026-09-20T00:00:00.000Z" },
]);
// A node the session never evaluated shows nulls, never a guess.
const fresh = fakeStores([node("x", "X", [pt("chat1")])], { events });
assert.deepEqual(graphSessionPropositions(fresh, "chat1"), [
{ nodeId: "x", name: "X", understanding: null, evaluatedAt: null },
{ nodeId: "a", name: "Alpha" },
{ nodeId: "b", name: "Beta" },
]);
});
});