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
+95 -6
View File
@@ -1,7 +1,7 @@
import { Type } from "typebox";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { serverExecute } from "../tool-execute.ts";
import { graphSessionIndex, graphSessionPropositions } from "../../core/graph.ts";
import { getGraph, graphSessionIndex, graphSessionPropositions } from "../../core/graph.ts";
import type { ChatStore, GraphStore } from "../../core/ports.ts";
/**
@@ -63,18 +63,107 @@ export function proberGraphTools(ctx: GraphToolContext): Record<string, AgentToo
name: "list_propositions",
label: "list_propositions",
description:
"List the propositions BORN in one knowledge-graph session — what was first learned there — each with his latest understanding level on the scale solid / shaky / accepted / resisted / not yet engaged, and the date that level was set. " +
"Names, levels, and dating only — no statement text, no session contents. " +
"The level is that session's own latest evaluation; other sessions may have moved it since, so cross-session levels can differ.",
"List the propositions BORN in one knowledge-graph session — what was covered there. " +
"Names only — no statement text, no session contents (owner ruling 2026-09-23: the map's ORGANIZATION is your business, its prose is not).",
parameters: listPropositionsInput,
execute: serverExecute("list_propositions", listPropositionsInput, async ({ session }): Promise<string> => {
const propositions = graphSessionPropositions(deps, session);
if (propositions.length === 0) {
return `No propositions born in session "${session}". Call list_sessions for the current ids.`;
}
const lines = propositions.map(
(p) => `- ${p.name} — ${p.understanding ?? "never evaluated"}${p.evaluatedAt ? ` (as of ${p.evaluatedAt})` : ""}`,
const lines = propositions.map((p) => `- ${p.name}`);
return `${propositions.length} proposition(s) covered in this session:\n${lines.join("\n")}`;
}),
},
};
}
// ─── the auditor's graph tools (2026-09-27 relink) ───────────────────────────
/**
* THE AUDITOR'S TOOLSET — the extraction auditor explores the graph to
* place new propositions instead of receiving it injected (owner ruling
* 2026-09-27: a full-graph dump bloats context as the graph grows; the
* auditor looks up identity candidates itself). Auditor-specific
* versions, deliberately different from the prober's:
*
* find glob over node names + statements + aliases — the
* identity workhorse; returns id + name + statement
* (statement text is ESSENTIAL for identity work,
* unlike the prober's names-only rule).
* list_sessions the birth-session index, no timestamps.
* list_propositions full proposition text (same reasoning as find).
*
* No timestamps anywhere: identity work needs content, not chronology.
* Read-only, pure core seams, same ports the snapshot reads.
*/
/** Glob match — `*` spans anything, case-insensitive. `*needle*` is the
* expected shape; a bare term gets implicit surrounding stars. */
function globMatch(pattern: string, text: string): boolean {
const raw = pattern.includes("*") ? pattern : `*${pattern}*`;
const re = new RegExp(
`^${raw.replace(/[.*+?^${}()|[\]\\]/g, (ch) => (ch === "*" ? "[\\s\\S]*" : `\\${ch}`))}$`,
"i",
);
return re.test(text);
}
export function auditorGraphTools(ctx: GraphToolContext): Record<string, AgentTool<any>> {
const deps = { chatStore: ctx.chatStore, graphStore: ctx.graphStore };
const findInput = Type.Object({
query: Type.String({
minLength: 1,
description: "Glob pattern over node names, statements, and aliases — e.g. *will to power*. A bare term gets implicit surrounding *.",
}),
});
return {
find: {
name: "find",
label: "find",
description:
"Search the knowledge graph for existing propositions — glob over names, statements, and aliases. " +
"Returns each match's id, name, and full statement. This is how you decide whether a new proposition IS an existing one: search its key words, its synonyms, its phrasings.",
parameters: findInput,
execute: serverExecute("find", findInput, async ({ query }): Promise<string> => {
const nodes = getGraph(deps.graphStore).nodes;
const hits = nodes.filter(
(n) => globMatch(query, n.name) || globMatch(query, n.statement) || n.aliases.some((a) => globMatch(query, a)),
);
if (hits.length === 0) return `No existing proposition matches "${query}".`;
const lines = hits.map((n) => `- ${n.id} :: ${n.name}\n statement: ${n.statement}`);
return `${hits.length} match(es):\n${lines.join("\n")}`;
}),
},
list_sessions: {
name: "list_sessions",
label: "list_sessions",
description:
"List the knowledge graph's birth sessions — every chat or imported-notes file that ever contributed a proposition. " +
"Each line: name, kind, proposition count, id. Use the id with list_propositions to see what a session covered.",
parameters: Type.Object({}),
execute: serverExecute("list_sessions", Type.Object({}), async (): Promise<string> => {
const sessions = graphSessionIndex(deps);
if (sessions.length === 0) return "The knowledge graph is empty — no session has contributed propositions yet.";
const lines = sessions.map((s, i) => `${i + 1}. ${s.name} (${s.kind}, ${s.propositions} proposition${s.propositions === 1 ? "" : "s"}) — id: ${s.id}`);
return `${sessions.length} session(s):\n${lines.join("\n")}`;
}),
},
list_propositions: {
name: "list_propositions",
label: "list_propositions",
description:
"List the propositions BORN in one session, with FULL statement text — the content-level view for identity work " +
"(deciding whether a new proposition is really one of these).",
parameters: listPropositionsInput,
execute: serverExecute("list_propositions", listPropositionsInput, async ({ session }): Promise<string> => {
const propositions = graphSessionPropositions(deps, session);
if (propositions.length === 0) return `No propositions born in session "${session}". Call list_sessions for the current ids.`;
const nodes = new Map(getGraph(deps.graphStore).nodes.map((n) => [n.id, n]));
const lines = propositions.map((p) => {
const node = nodes.get(p.nodeId);
return `- ${p.nodeId} :: ${p.name}\n statement: ${node?.statement ?? "(unknown)"}`;
});
return `${propositions.length} proposition(s) born in this session:\n${lines.join("\n")}`;
}),
},