omo-memory 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/graphTui.js DELETED
@@ -1,239 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { BoxRenderable, createCliRenderer, TextRenderable } from "@opentui/core";
3
- const SNAPSHOT_ENV = "OMO_MEMORY_GRAPH_TUI_SNAPSHOT";
4
- const COLORS = {
5
- background: "#101419",
6
- border: "#516071",
7
- detail: "#17212b",
8
- text: "#d8dee9",
9
- muted: "#94a3b8",
10
- accent: "#7dd3fc",
11
- };
12
- export async function runGraphTui(options) {
13
- const existingSnapshot = parseSnapshot(process.env[SNAPSHOT_ENV]);
14
- if (process.versions["bun"] === undefined) {
15
- const graphSnapshot = existingSnapshot ?? (await createGraphSnapshot(options));
16
- await runWithBun(options, graphSnapshot);
17
- return;
18
- }
19
- if (existingSnapshot === null) {
20
- throw new Error("graph tui OpenTUI renderer requires a graph snapshot");
21
- }
22
- const renderer = await createCliRenderer({
23
- targetFps: 12,
24
- maxFps: 12,
25
- screenMode: "main-screen",
26
- consoleMode: "disabled",
27
- clearOnShutdown: false,
28
- });
29
- renderer.setTerminalTitle("OMO Ontology Graph");
30
- let state = { options, selectedId: null, snapshot: existingSnapshot };
31
- let graph = loadGraph(state);
32
- state = { ...state, selectedId: graph.detail?.id ?? null };
33
- const root = new BoxRenderable(renderer, {
34
- id: "graph-root",
35
- width: "100%",
36
- height: "100%",
37
- flexDirection: "column",
38
- backgroundColor: COLORS.background,
39
- padding: 1,
40
- gap: 1,
41
- });
42
- const title = new TextRenderable(renderer, { id: "graph-title", content: "", fg: COLORS.accent, height: 1, truncate: true });
43
- const body = new BoxRenderable(renderer, { id: "graph-body", flexGrow: 1, flexDirection: "row", gap: 1 });
44
- const nodesPane = new BoxRenderable(renderer, {
45
- id: "graph-nodes-pane",
46
- width: "50%",
47
- height: "100%",
48
- border: true,
49
- borderColor: COLORS.border,
50
- title: "Nodes",
51
- padding: 1,
52
- backgroundColor: COLORS.background,
53
- });
54
- const detailPane = new BoxRenderable(renderer, {
55
- id: "graph-detail-pane",
56
- flexGrow: 1,
57
- height: "100%",
58
- border: true,
59
- borderColor: COLORS.border,
60
- title: "Detail Pane",
61
- padding: 1,
62
- backgroundColor: COLORS.detail,
63
- });
64
- const nodesText = new TextRenderable(renderer, { id: "graph-nodes", content: "", fg: COLORS.text, wrapMode: "word" });
65
- const detailText = new TextRenderable(renderer, { id: "graph-detail", content: "", fg: COLORS.text, wrapMode: "word" });
66
- const footer = new TextRenderable(renderer, { id: "graph-footer", content: "", fg: COLORS.muted, height: 1, truncate: true });
67
- nodesPane.add(nodesText);
68
- detailPane.add(detailText);
69
- body.add(nodesPane);
70
- body.add(detailPane);
71
- root.add(title);
72
- root.add(body);
73
- root.add(footer);
74
- renderer.root.add(root);
75
- const render = () => {
76
- graph = loadGraph(state);
77
- state = { ...state, selectedId: graph.detail?.id ?? null };
78
- title.content = titleText(graph, state.options.query);
79
- nodesText.content = nodesContent(graph);
80
- detailText.content = detailContent(graph);
81
- footer.content = "q quit | ArrowUp/ArrowDown/Tab select | Legend: D durable, W working, T temporary, E ephemeral";
82
- renderer.requestRender();
83
- };
84
- await new Promise((resolve) => {
85
- const quit = () => {
86
- renderer.keyInput.removeAllListeners("keypress");
87
- renderer.destroy();
88
- resolve();
89
- };
90
- renderer.keyInput.on("keypress", (key) => {
91
- if (key.name === "q" || (key.name === "c" && key.ctrl)) {
92
- quit();
93
- return;
94
- }
95
- const nextId = nextSelectedId(graph.nodes, state.selectedId, key.name);
96
- if (nextId !== state.selectedId) {
97
- state = { ...state, selectedId: nextId };
98
- render();
99
- }
100
- });
101
- writeCaptureFrame(graph, state.options.query);
102
- renderer.start();
103
- render();
104
- });
105
- }
106
- async function runWithBun(options, snapshot) {
107
- const scriptPath = process.argv[1];
108
- if (scriptPath === undefined)
109
- throw new Error("graph tui requires a CLI script path");
110
- const args = [scriptPath, "graph", "tui", "--db", options.dbPath];
111
- if (options.query !== undefined)
112
- args.push("--query", options.query);
113
- await new Promise((resolve, reject) => {
114
- const child = spawn("bun", args, {
115
- stdio: "inherit",
116
- env: { ...process.env, [SNAPSHOT_ENV]: JSON.stringify(snapshot) },
117
- });
118
- child.once("error", (error) => {
119
- reject(new Error(`graph tui requires Bun for OpenTUI native FFI: ${error.message}`));
120
- });
121
- child.once("exit", (code, signal) => {
122
- if (code === 0) {
123
- resolve();
124
- return;
125
- }
126
- reject(new Error(`graph tui OpenTUI runtime exited with ${signal ?? code ?? "unknown status"}`));
127
- });
128
- });
129
- }
130
- async function createGraphSnapshot(options) {
131
- const { projectOntologyGraph } = await import("./ontologyGraph.js");
132
- const graph = projectOntologyGraph({
133
- dbPath: options.dbPath,
134
- ...(options.query === undefined ? {} : { query: options.query }),
135
- });
136
- const details = graph.nodes
137
- .map((node) => projectOntologyGraph({
138
- dbPath: options.dbPath,
139
- ...(options.query === undefined ? {} : { query: options.query }),
140
- selectedId: node.id,
141
- }).detail)
142
- .filter((detail) => detail !== null);
143
- return { graph, details };
144
- }
145
- function parseSnapshot(raw) {
146
- if (raw === undefined)
147
- return null;
148
- const parsed = JSON.parse(raw);
149
- if (!isGraphSnapshot(parsed))
150
- throw new Error("invalid graph tui snapshot");
151
- return parsed;
152
- }
153
- function isRecord(value) {
154
- return value !== null && typeof value === "object";
155
- }
156
- function isGraphSnapshot(value) {
157
- if (!isRecord(value))
158
- return false;
159
- return isRecord(value["graph"]) && Array.isArray(value["details"]);
160
- }
161
- function loadGraph(state) {
162
- const selectedId = state.selectedId ?? state.snapshot.graph.detail?.id ?? state.snapshot.graph.nodes[0]?.id ?? null;
163
- const detail = selectedId === null ? state.snapshot.graph.detail : (state.snapshot.details.find((item) => item.id === selectedId) ?? null);
164
- return {
165
- ...state.snapshot.graph,
166
- nodes: state.snapshot.graph.nodes.map((node) => ({ ...node, selected: node.id === selectedId })),
167
- detail,
168
- };
169
- }
170
- function titleText(graph, query) {
171
- const queryLabel = query === undefined ? "all concepts" : `query "${query}"`;
172
- return `OMO Ontology Graph - ${queryLabel} - ${graph.nodes.length} nodes / ${graph.edges.length} edges`;
173
- }
174
- function nodesContent(graph) {
175
- if (graph.nodes.length === 0)
176
- return graph.message ?? "No ontology graph data is available yet.";
177
- const edges = graph.edges.map((edge) => ` ${edge.sourceId} -> ${edge.targetId} [${edge.label}]`);
178
- return [...graph.nodes.map((node) => nodeLine(node)), "", "Relations", ...(edges.length === 0 ? [" none in current filter"] : edges)].join("\n");
179
- }
180
- function detailContent(graph) {
181
- if (graph.detail === null)
182
- return graph.message ?? "No ontology graph data is available yet.";
183
- const detail = graph.detail;
184
- return [
185
- `Label: ${detail.label}`,
186
- `Kind: ${detail.kind}`,
187
- `Retention: ${classCode(detail.retentionClass)} ${detail.retentionClass}`,
188
- `Score: ${detail.scoreLabel}`,
189
- `Refs: ${detail.refCount}`,
190
- `Projects: ${detail.projectSpread}`,
191
- `First seen: ${detail.firstSeen ?? "unknown"}`,
192
- `Last seen: ${detail.lastSeen ?? "unknown"}`,
193
- `Project: ${detail.project.repoRoot}`,
194
- `Remote: ${detail.project.gitRemote ?? "none"}`,
195
- "",
196
- "Description:",
197
- detail.description ?? "none",
198
- "",
199
- `Aliases: ${detail.aliases.length === 0 ? "none" : detail.aliases.join(", ")}`,
200
- ].join("\n");
201
- }
202
- function writeCaptureFrame(graph, query) {
203
- process.stdout.write([
204
- titleText(graph, query),
205
- "",
206
- "Nodes",
207
- nodesContent(graph),
208
- "",
209
- "Detail",
210
- detailContent(graph),
211
- "",
212
- "Legend: D durable, W working, T temporary, E ephemeral",
213
- "",
214
- ].join("\n"));
215
- }
216
- function nodeLine(node) {
217
- const marker = node.selected ? ">" : " ";
218
- return `${marker} ${classCode(node.retentionClass)} ${node.label} (${node.kind}, ${node.scoreLabel}, refs ${node.refCount})`;
219
- }
220
- function classCode(retentionClass) {
221
- const normalized = retentionClass.toLowerCase();
222
- if (normalized === "durable")
223
- return "D";
224
- if (normalized === "temporary")
225
- return "T";
226
- if (normalized === "ephemeral")
227
- return "E";
228
- return "W";
229
- }
230
- function nextSelectedId(nodes, selectedId, keyName) {
231
- if (nodes.length === 0)
232
- return null;
233
- const currentIndex = Math.max(0, nodes.findIndex((node) => node.id === selectedId));
234
- const delta = keyName === "up" ? -1 : keyName === "down" || keyName === "tab" ? 1 : 0;
235
- if (delta === 0)
236
- return selectedId;
237
- const nextIndex = (currentIndex + delta + nodes.length) % nodes.length;
238
- return nodes[nextIndex]?.id ?? selectedId;
239
- }
@@ -1,117 +0,0 @@
1
- import { z } from "zod";
2
- import { applyConceptExtraction, extractConceptCandidates } from "./conceptExtraction.js";
3
- import { listGlobalMemory, migrateToGlobalMemory, scanForMemoryDbs } from "./globalMemory.js";
4
- import { memoryPaths } from "./memoryReport.js";
5
- import { createDurableMemory, listOntologyRows, supersedeDurableMemory, updateDurableRetention } from "./ontologyCore.js";
6
- import { resolveProjectContext } from "./projectContext.js";
7
- import { recomputeRetentionScores } from "./retentionRecompute.js";
8
- const retentionClassSchema = z.enum(["forget", "temporary", "working", "durable", "permanent"]);
9
- const nonBlankStringSchema = z.string().trim().min(1);
10
- export function registerGlobalOntologyTools(server) {
11
- server.registerTool("memory_global_scan", {
12
- title: "Scan Global OMO Memory Sources",
13
- description: "Explicitly scan a filesystem root for local OMO memory SQLite databases without importing them.",
14
- inputSchema: { rootPath: nonBlankStringSchema },
15
- }, async ({ rootPath }) => jsonResult(scanForMemoryDbs(rootPath)));
16
- server.registerTool("memory_global_migrate", {
17
- title: "Migrate OMO Memory To Global SQLite",
18
- description: "Explicitly create or update a global OMO memory SQLite database from discovered local memory databases.",
19
- inputSchema: { rootPath: nonBlankStringSchema, globalDbPath: nonBlankStringSchema },
20
- }, async ({ rootPath, globalDbPath }) => jsonResult(migrateToGlobalMemory({ rootPath, globalDbPath })));
21
- server.registerTool("memory_global_list", {
22
- title: "List Global OMO Memory",
23
- description: "List sources and counts from an explicit global OMO memory SQLite database.",
24
- inputSchema: { globalDbPath: nonBlankStringSchema },
25
- }, async ({ globalDbPath }) => jsonResult(listGlobalMemory(globalDbPath)));
26
- registerOntologyTools(server);
27
- }
28
- function registerOntologyTools(server) {
29
- server.registerTool("memory_ontology_candidates", {
30
- title: "Extract OMO Ontology Candidates",
31
- description: "Return deterministic ontology candidate labels from a short event summary without writing rows.",
32
- inputSchema: { summary: nonBlankStringSchema, eventType: nonBlankStringSchema.optional() },
33
- }, async ({ summary, eventType }) => jsonResult({ candidates: extractConceptCandidates(summary, eventType) }));
34
- server.registerTool("memory_ontology_extract", {
35
- title: "Write OMO Ontology Extraction",
36
- description: "Explicitly extract concepts from a short event summary and write event-to-concept references.",
37
- inputSchema: {
38
- sourceEventId: nonBlankStringSchema,
39
- summary: nonBlankStringSchema,
40
- eventType: nonBlankStringSchema.optional(),
41
- },
42
- }, async ({ sourceEventId, summary, eventType }) => jsonResult(applyConceptExtraction(memoryPaths().dbPath, resolveProjectContext(), sourceEventId, summary, eventType)));
43
- server.registerTool("memory_ontology_score", {
44
- title: "Recompute OMO Ontology Scores",
45
- description: "Explicitly recompute ontology retention scores for the local OMO memory SQLite database.",
46
- inputSchema: { nowIso: z.string().datetime().optional() },
47
- }, async ({ nowIso }) => jsonResult(recomputeRetentionScores({ dbPath: memoryPaths().dbPath, nowIso: nowIso ?? new Date().toISOString() })));
48
- server.registerTool("memory_ontology_promote", {
49
- title: "Promote OMO Durable Memory",
50
- description: "Explicitly promote a summarized memory into durable ontology storage.",
51
- inputSchema: {
52
- type: nonBlankStringSchema,
53
- summary: nonBlankStringSchema,
54
- body: nonBlankStringSchema.optional(),
55
- sourceEventId: nonBlankStringSchema.optional(),
56
- sourceHandoffId: nonBlankStringSchema.optional(),
57
- confidence: z.number().min(0).max(1).optional(),
58
- status: nonBlankStringSchema.optional(),
59
- retentionClass: retentionClassSchema.optional(),
60
- },
61
- }, async ({ type, summary, body, sourceEventId, sourceHandoffId, confidence, status, retentionClass }) => jsonResult(createDurableMemory(memoryPaths().dbPath, resolveProjectContext(), {
62
- type,
63
- summary,
64
- ...(body === undefined ? {} : { body }),
65
- ...(sourceEventId === undefined ? {} : { sourceEventId }),
66
- ...(sourceHandoffId === undefined ? {} : { sourceHandoffId }),
67
- ...(confidence === undefined ? {} : { confidence }),
68
- ...(status === undefined ? {} : { status }),
69
- ...(retentionClass === undefined ? {} : { retentionClass }),
70
- })));
71
- server.registerTool("memory_ontology_demote", {
72
- title: "Demote OMO Durable Memory",
73
- description: "Explicitly change the retention class for a durable ontology memory.",
74
- inputSchema: { durableId: nonBlankStringSchema, retentionClass: retentionClassSchema.default("temporary") },
75
- }, async ({ durableId, retentionClass }) => jsonResult(updateDurableRetention(memoryPaths().dbPath, resolveProjectContext(), durableId, { retentionClass })));
76
- server.registerTool("memory_ontology_supersede", {
77
- title: "Supersede OMO Durable Memory",
78
- description: "Explicitly mark a durable memory superseded and create a successor memory.",
79
- inputSchema: {
80
- durableId: nonBlankStringSchema,
81
- reason: nonBlankStringSchema.optional(),
82
- newSummary: nonBlankStringSchema.optional(),
83
- },
84
- }, async ({ durableId, reason, newSummary }) => jsonResult(supersedeDurableMemory(memoryPaths().dbPath, resolveProjectContext(), durableId, {
85
- ...(reason === undefined ? {} : { reason }),
86
- ...(newSummary === undefined ? {} : { newSummary }),
87
- })));
88
- server.registerTool("memory_ontology_recall", {
89
- title: "Recall OMO Ontology Rows",
90
- description: "Explicitly recall ontology concepts and durable memories matching a query.",
91
- inputSchema: { query: nonBlankStringSchema, limit: z.number().int().positive().max(100).default(10) },
92
- }, async ({ query, limit }) => jsonResult(recallOntology(query, limit)));
93
- }
94
- function jsonResult(value) {
95
- return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
96
- }
97
- function recallOntology(query, limit) {
98
- const rows = listOntologyRows(memoryPaths().dbPath, resolveProjectContext());
99
- const normalizedQuery = query.toLowerCase();
100
- const queryTerms = normalizedQuery.split(/[^a-z0-9_-]+/).filter((term) => term.length >= 3);
101
- return {
102
- concepts: rows.concepts.filter((concept) => matchesConcept(concept, normalizedQuery, queryTerms)).slice(0, limit),
103
- durableMemories: rows.durableMemories.filter((memory) => matchesDurableMemory(memory, normalizedQuery, queryTerms)).slice(0, limit),
104
- };
105
- }
106
- function matchesConcept(concept, normalizedQuery, queryTerms) {
107
- return matchesText([concept.label, concept.description ?? "", concept.aliasesJson], normalizedQuery, queryTerms);
108
- }
109
- function matchesDurableMemory(memory, normalizedQuery, queryTerms) {
110
- return matchesText([memory.type, memory.summary, memory.body ?? ""], normalizedQuery, queryTerms);
111
- }
112
- function matchesText(values, normalizedQuery, queryTerms) {
113
- const normalizedValues = values.map((value) => value.toLowerCase());
114
- if (normalizedValues.some((value) => value.includes(normalizedQuery)))
115
- return true;
116
- return queryTerms.length > 0 && queryTerms.every((term) => normalizedValues.some((value) => value.includes(term)));
117
- }
@@ -1,142 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { upsertProject } from "./memory.js";
3
- import { migrate, openMemoryDb } from "./memoryDb.js";
4
- import { redactSecrets } from "./privacy.js";
5
- export { listOntologyRows } from "./ontologyQueries.js";
6
- export { supersedeDurableMemory } from "./ontologySupersede.js";
7
- function nowIso() {
8
- return new Date().toISOString();
9
- }
10
- function normalizeLabel(label) {
11
- return label.trim().toLowerCase();
12
- }
13
- export function upsertConcept(dbPath, project, input) {
14
- const db = openMemoryDb(dbPath);
15
- try {
16
- migrate(db);
17
- upsertProject(db, project);
18
- const label = normalizeLabel(input.label);
19
- const now = nowIso();
20
- const existing = db.prepare(`SELECT id FROM concepts WHERE project_id = ? AND label = ? LIMIT 1`).get(project.id, label);
21
- if (existing?.id) {
22
- db.prepare("UPDATE concepts SET last_seen = ?, updated_at = ? WHERE id = ? AND project_id = ?").run(now, now, existing.id, project.id);
23
- const row = db
24
- .prepare(`SELECT id, kind, label, description, aliases_json AS aliasesJson, payload_json AS payloadJson,
25
- valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt,
26
- COALESCE(score, 0) AS score, COALESCE(retention_class, 'working') AS retentionClass,
27
- COALESCE(manual_pin, 0) AS manualPin, COALESCE(ref_count, 0) AS refCount,
28
- COALESCE(project_spread, 1) AS projectSpread, first_seen AS firstSeen, last_seen AS lastSeen
29
- FROM concepts WHERE id = ?`)
30
- .get(existing.id);
31
- return row;
32
- }
33
- const id = randomUUID();
34
- const score = input.score ?? 0;
35
- const retentionClass = input.retentionClass ?? "working";
36
- const manualPin = input.manualPin ?? 0;
37
- const created = now;
38
- db.prepare(`
39
- INSERT INTO concepts (
40
- id, project_id, kind, label, description, aliases_json, payload_json,
41
- valid_from, valid_to, created_at, updated_at,
42
- score, retention_class, manual_pin, ref_count, project_spread, first_seen, last_seen
43
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
44
- `).run(id, project.id, input.kind, label, input.description ?? null, "[]", "{}", null, null, created, created, score, retentionClass, manualPin, 0, 1, created, created);
45
- const row = db
46
- .prepare(`SELECT id, kind, label, description, aliases_json AS aliasesJson, payload_json AS payloadJson,
47
- valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt,
48
- COALESCE(score, 0) AS score, COALESCE(retention_class, 'working') AS retentionClass,
49
- COALESCE(manual_pin, 0) AS manualPin, COALESCE(ref_count, 0) AS refCount,
50
- COALESCE(project_spread, 1) AS projectSpread, first_seen AS firstSeen, last_seen AS lastSeen
51
- FROM concepts WHERE id = ?`)
52
- .get(id);
53
- return row;
54
- }
55
- finally {
56
- db.close();
57
- }
58
- }
59
- export function createDurableMemory(dbPath, project, input) {
60
- const db = openMemoryDb(dbPath);
61
- try {
62
- migrate(db);
63
- upsertProject(db, project);
64
- const id = randomUUID();
65
- const created = nowIso();
66
- const redactedSummary = redactSecrets(input.summary);
67
- const redactedBody = input.body == null ? null : redactSecrets(input.body);
68
- const status = input.status ?? "active";
69
- const retentionClass = input.retentionClass ?? "durable";
70
- const confidence = input.confidence ?? 0;
71
- db.prepare(`
72
- INSERT INTO durable_memories (
73
- id, project_id, type, summary, body, source_event_id, source_handoff_id,
74
- confidence, status, retention_class, valid_from, valid_to, created_at, updated_at
75
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
76
- `).run(id, project.id, input.type, redactedSummary, redactedBody, input.sourceEventId ?? null, input.sourceHandoffId ?? null, confidence, status, retentionClass, null, null, created, created);
77
- const row = db
78
- .prepare(`SELECT id, type, summary, body, source_event_id AS sourceEventId, source_handoff_id AS sourceHandoffId,
79
- confidence, status, COALESCE(retention_class, 'durable') AS retentionClass,
80
- valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
81
- FROM durable_memories WHERE id = ?`)
82
- .get(id);
83
- return row;
84
- }
85
- finally {
86
- db.close();
87
- }
88
- }
89
- export function recordMemoryReference(dbPath, project, input) {
90
- const db = openMemoryDb(dbPath);
91
- try {
92
- migrate(db);
93
- upsertProject(db, project);
94
- const id = randomUUID();
95
- const created = nowIso();
96
- const refKind = input.refKind ?? "mentions";
97
- const weight = input.weight ?? 1;
98
- const inserted = db
99
- .prepare(`
100
- INSERT INTO memory_references (
101
- id, project_id, source_type, source_id, target_type, target_id, ref_kind, weight, created_at
102
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
103
- ON CONFLICT(project_id, source_type, source_id, target_type, target_id, ref_kind) DO NOTHING
104
- `)
105
- .run(id, project.id, input.sourceType, input.sourceId, input.targetType, input.targetId, refKind, weight, created);
106
- if (inserted.changes > 0 && input.targetType === "concept") {
107
- db.prepare("UPDATE concepts SET ref_count = COALESCE(ref_count, 0) + 1, last_seen = ?, updated_at = ? WHERE id = ? AND project_id = ?").run(created, created, input.targetId, project.id);
108
- }
109
- const row = db
110
- .prepare(`SELECT id, source_type AS sourceType, source_id AS sourceId, target_type AS targetType, target_id AS targetId,
111
- ref_kind AS refKind, weight, created_at AS createdAt
112
- FROM memory_references
113
- WHERE project_id = ? AND source_type = ? AND source_id = ? AND target_type = ? AND target_id = ? AND ref_kind = ?`)
114
- .get(project.id, input.sourceType, input.sourceId, input.targetType, input.targetId, refKind);
115
- return row;
116
- }
117
- finally {
118
- db.close();
119
- }
120
- }
121
- export function updateDurableRetention(dbPath, project, durableId, update) {
122
- const db = openMemoryDb(dbPath);
123
- try {
124
- migrate(db);
125
- const existing = db.prepare("SELECT retention_class FROM durable_memories WHERE id = ? AND project_id = ?").get(durableId, project.id);
126
- if (!existing) {
127
- throw new Error("durable memory not found for project");
128
- }
129
- const now = nowIso();
130
- db.prepare("UPDATE durable_memories SET retention_class = COALESCE(?, retention_class), updated_at = ? WHERE id = ? AND project_id = ?").run(update.retentionClass ?? null, now, durableId, project.id);
131
- const row = db
132
- .prepare(`SELECT id, type, summary, body, source_event_id AS sourceEventId, source_handoff_id AS sourceHandoffId,
133
- confidence, status, COALESCE(retention_class, 'durable') AS retentionClass,
134
- valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
135
- FROM durable_memories WHERE id = ?`)
136
- .get(durableId);
137
- return row;
138
- }
139
- finally {
140
- db.close();
141
- }
142
- }
@@ -1,173 +0,0 @@
1
- import { migrate, openMemoryDb } from "./memoryDb.js";
2
- import { redactSecrets, sanitizeGitRemote } from "./privacy.js";
3
- function isRecord(value) {
4
- return value !== null && typeof value === "object";
5
- }
6
- function text(value) {
7
- return typeof value === "string" ? value : String(value ?? "");
8
- }
9
- function nullableText(value) {
10
- return value === null || value === undefined ? null : text(value);
11
- }
12
- function parseAliases(value) {
13
- if (typeof value !== "string")
14
- return [];
15
- try {
16
- const parsed = JSON.parse(value);
17
- if (!Array.isArray(parsed))
18
- return [];
19
- return parsed.filter((item) => typeof item === "string").map(redactSecrets);
20
- }
21
- catch (error) {
22
- if (error instanceof SyntaxError)
23
- return [];
24
- throw error;
25
- }
26
- }
27
- function numberValue(value) {
28
- return typeof value === "number" && Number.isFinite(value) ? value : Number(value ?? 0);
29
- }
30
- function projectFrom(row) {
31
- return {
32
- id: row.projectId,
33
- repoRoot: redactSecrets(row.repoRoot),
34
- gitRemote: sanitizeGitRemote(row.gitRemote),
35
- };
36
- }
37
- function parseConceptRow(value) {
38
- if (!isRecord(value)) {
39
- throw new Error("invalid concept row");
40
- }
41
- return {
42
- id: text(value["id"]),
43
- projectId: text(value["projectId"]),
44
- kind: text(value["kind"]),
45
- label: text(value["label"]),
46
- description: nullableText(value["description"]),
47
- aliases: parseAliases(value["aliasesJson"]),
48
- score: numberValue(value["score"]),
49
- retentionClass: text(value["retentionClass"]),
50
- refCount: numberValue(value["refCount"]),
51
- projectSpread: numberValue(value["projectSpread"]),
52
- firstSeen: nullableText(value["firstSeen"]),
53
- lastSeen: nullableText(value["lastSeen"]),
54
- repoRoot: text(value["repoRoot"]),
55
- gitRemote: nullableText(value["gitRemote"]),
56
- };
57
- }
58
- function parseRelationRow(value) {
59
- if (!isRecord(value)) {
60
- throw new Error("invalid relation row");
61
- }
62
- return {
63
- id: text(value["id"]),
64
- projectId: text(value["projectId"]),
65
- sourceId: text(value["sourceId"]),
66
- targetId: text(value["targetId"]),
67
- relation: text(value["relation"]),
68
- weight: numberValue(value["weight"]),
69
- repoRoot: text(value["repoRoot"]),
70
- gitRemote: nullableText(value["gitRemote"]),
71
- };
72
- }
73
- function matchesQuery(row, query) {
74
- const haystack = `${row.label}\n${row.description ?? ""}\n${row.aliases.join("\n")}`.toLowerCase();
75
- return haystack.includes(query);
76
- }
77
- function toNode(row, selectedId) {
78
- const score = Math.round(row.score);
79
- const retentionClass = redactSecrets(row.retentionClass);
80
- return {
81
- id: row.id,
82
- kind: redactSecrets(row.kind),
83
- label: redactSecrets(row.label),
84
- description: row.description === null ? null : redactSecrets(row.description),
85
- aliases: row.aliases,
86
- retentionClass,
87
- score,
88
- scoreLabel: `${score} ${retentionClass}`,
89
- refCount: Math.round(row.refCount),
90
- projectSpread: Math.round(row.projectSpread),
91
- project: projectFrom(row),
92
- selected: selectedId === row.id,
93
- };
94
- }
95
- function toDetail(row) {
96
- const score = Math.round(row.score);
97
- const retentionClass = redactSecrets(row.retentionClass);
98
- return {
99
- id: row.id,
100
- label: redactSecrets(row.label),
101
- kind: redactSecrets(row.kind),
102
- description: row.description === null ? null : redactSecrets(row.description),
103
- aliases: row.aliases,
104
- retentionClass,
105
- score,
106
- scoreLabel: `${score} ${retentionClass}`,
107
- refCount: Math.round(row.refCount),
108
- projectSpread: Math.round(row.projectSpread),
109
- firstSeen: row.firstSeen,
110
- lastSeen: row.lastSeen,
111
- project: projectFrom(row),
112
- };
113
- }
114
- function toEdge(row) {
115
- const weight = Number(row.weight.toFixed(2));
116
- const relation = redactSecrets(row.relation);
117
- return {
118
- id: row.id,
119
- sourceId: row.sourceId,
120
- targetId: row.targetId,
121
- relation,
122
- label: `${relation} ${weight.toFixed(2)}`,
123
- weight,
124
- project: projectFrom(row),
125
- };
126
- }
127
- export function projectOntologyGraph(options) {
128
- const db = openMemoryDb(options.dbPath);
129
- try {
130
- migrate(db);
131
- const query = options.query?.trim().toLowerCase() ?? "";
132
- const conceptRows = db
133
- .prepare(`
134
- SELECT c.id, c.project_id AS projectId, c.kind, c.label, c.description, c.aliases_json AS aliasesJson,
135
- COALESCE(c.score, 0) AS score, COALESCE(c.retention_class, 'working') AS retentionClass,
136
- COALESCE(c.ref_count, 0) AS refCount, COALESCE(c.project_spread, 1) AS projectSpread,
137
- c.first_seen AS firstSeen, c.last_seen AS lastSeen,
138
- p.repo_root AS repoRoot, p.git_remote AS gitRemote
139
- FROM concepts c
140
- JOIN projects p ON p.id = c.project_id
141
- WHERE c.valid_to IS NULL
142
- ORDER BY lower(c.label) ASC, c.id ASC
143
- `)
144
- .all()
145
- .map(parseConceptRow)
146
- .filter((row) => query === "" || matchesQuery(row, query));
147
- const conceptIds = new Set(conceptRows.map((row) => row.id));
148
- const selectedId = options.selectedId && conceptIds.has(options.selectedId) ? options.selectedId : (conceptRows[0]?.id ?? null);
149
- const nodes = conceptRows.map((row) => toNode(row, selectedId));
150
- const selectedRow = selectedId === null ? undefined : conceptRows.find((row) => row.id === selectedId);
151
- const relationRows = db
152
- .prepare(`
153
- SELECT r.id, r.project_id AS projectId, r.source_id AS sourceId, r.target_id AS targetId,
154
- r.relation, COALESCE(r.weight, 1) AS weight, p.repo_root AS repoRoot, p.git_remote AS gitRemote
155
- FROM relations r
156
- JOIN projects p ON p.id = r.project_id
157
- WHERE r.source_type = 'concept' AND r.target_type = 'concept' AND r.valid_to IS NULL
158
- ORDER BY lower(r.relation) ASC, r.id ASC
159
- `)
160
- .all()
161
- .map(parseRelationRow)
162
- .filter((row) => conceptIds.has(row.sourceId) && conceptIds.has(row.targetId));
163
- return {
164
- nodes,
165
- edges: relationRows.map(toEdge),
166
- detail: selectedRow === undefined ? null : toDetail(selectedRow),
167
- message: nodes.length === 0 ? "No ontology graph data is available yet." : null,
168
- };
169
- }
170
- finally {
171
- db.close();
172
- }
173
- }