gsd-pi 2.75.0-dev.2203010a0 → 2.75.0-dev.96d4bb599

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.
Files changed (109) hide show
  1. package/dist/onboarding.d.ts +5 -1
  2. package/dist/onboarding.js +5 -3
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  4. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
  5. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  7. package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
  8. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
  9. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  10. package/dist/resources/extensions/gsd/commands-memory.js +462 -0
  11. package/dist/resources/extensions/gsd/gsd-db.js +237 -4
  12. package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
  13. package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
  14. package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
  15. package/dist/resources/extensions/gsd/memory-relations.js +189 -0
  16. package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
  17. package/dist/resources/extensions/gsd/memory-store.js +299 -6
  18. package/dist/resources/extensions/gsd/model-router.js +9 -5
  19. package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
  20. package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
  21. package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  53. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  54. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  55. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  56. package/package.json +1 -1
  57. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  58. package/packages/mcp-server/dist/server.js +12 -10
  59. package/packages/mcp-server/dist/server.js.map +1 -1
  60. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/session-manager.js +8 -1
  62. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  63. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
  64. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.js +113 -14
  66. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  67. package/packages/mcp-server/src/mcp-server.test.ts +40 -4
  68. package/packages/mcp-server/src/server.ts +12 -10
  69. package/packages/mcp-server/src/session-manager.ts +10 -3
  70. package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
  71. package/packages/mcp-server/src/workflow-tools.ts +128 -18
  72. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  73. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  74. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
  75. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
  76. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  77. package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
  78. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
  79. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  80. package/src/resources/extensions/gsd/commands-memory.ts +551 -0
  81. package/src/resources/extensions/gsd/gsd-db.ts +273 -4
  82. package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
  83. package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
  84. package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
  85. package/src/resources/extensions/gsd/memory-relations.ts +240 -0
  86. package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
  87. package/src/resources/extensions/gsd/memory-store.ts +351 -7
  88. package/src/resources/extensions/gsd/model-router.ts +10 -5
  89. package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
  90. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  91. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  92. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  93. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
  94. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  95. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  96. package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
  97. package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
  98. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
  99. package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
  100. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
  102. package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
  103. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
  104. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
  105. package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
  106. package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
  107. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  108. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
  109. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_ssgManifest.js +0 -0
@@ -0,0 +1,218 @@
1
+ // GSD Memory Ingest — turn raw content into memories
2
+ //
3
+ // Provides four entry points: ingestNote (inline text), ingestFile (local
4
+ // path), ingestUrl (HTTP resource), and ingestArtifact (a named .gsd/ artifact
5
+ // for a given milestone). Each one inserts a row into `memory_sources` and,
6
+ // if an LLM call is available, fires the extractor against the content with
7
+ // source-specific scope/tags.
8
+ //
9
+ // All four functions are safe to call without an LLM — they still persist the
10
+ // source. This means ingestion is decoupled from extraction; a later
11
+ // `/gsd memory rebuild` can re-extract from persisted sources.
12
+ import { existsSync, readFileSync, statSync } from "node:fs";
13
+ import { basename, isAbsolute, resolve } from "node:path";
14
+ import { createMemorySource } from "./memory-source-store.js";
15
+ import { buildMemoryLLMCall, extractMemoriesFromTranscript } from "./memory-extractor.js";
16
+ import { resolveMilestoneFile } from "./paths.js";
17
+ import { logWarning } from "./workflow-logger.js";
18
+ const DEFAULT_MAX_BYTES = 256 * 1024;
19
+ function truncate(content, maxBytes) {
20
+ const buf = Buffer.from(content, "utf-8");
21
+ if (buf.byteLength <= maxBytes)
22
+ return content;
23
+ return `${buf.subarray(0, maxBytes).toString("utf-8")}\n\n…[truncated to ${maxBytes} bytes]`;
24
+ }
25
+ async function maybeExtract(ctx, source, content, opts) {
26
+ if (opts.extract === false || !ctx)
27
+ return [];
28
+ const llmCallFn = buildMemoryLLMCall(ctx);
29
+ if (!llmCallFn)
30
+ return [];
31
+ try {
32
+ return await extractMemoriesFromTranscript(content, llmCallFn, {
33
+ sourceType: source.kind,
34
+ sourceId: source.id,
35
+ scope: opts.scope,
36
+ tags: opts.tags,
37
+ force: true,
38
+ });
39
+ }
40
+ catch (err) {
41
+ logWarning("memory-ingest", `extraction failed: ${err.message}`);
42
+ return [];
43
+ }
44
+ }
45
+ function sourceCreateFailure(kind) {
46
+ return {
47
+ sourceId: "",
48
+ duplicate: false,
49
+ extracted: [],
50
+ kind,
51
+ title: null,
52
+ uri: null,
53
+ };
54
+ }
55
+ // ─── ingestNote ─────────────────────────────────────────────────────────────
56
+ export async function ingestNote(note, ctx, opts = {}) {
57
+ const trimmed = note.trim();
58
+ if (!trimmed)
59
+ return sourceCreateFailure("note");
60
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
61
+ const content = truncate(trimmed, maxBytes);
62
+ const created = createMemorySource({
63
+ kind: "note",
64
+ uri: null,
65
+ title: content.slice(0, 80).replace(/\s+/g, " ").trim(),
66
+ content,
67
+ scope: opts.scope,
68
+ tags: opts.tags,
69
+ });
70
+ if (!created)
71
+ return sourceCreateFailure("note");
72
+ const extracted = created.duplicate
73
+ ? []
74
+ : await maybeExtract(ctx, { kind: "note", id: created.id }, content, opts);
75
+ return {
76
+ sourceId: created.id,
77
+ duplicate: created.duplicate,
78
+ extracted,
79
+ kind: "note",
80
+ title: content.slice(0, 80),
81
+ uri: null,
82
+ };
83
+ }
84
+ // ─── ingestFile ─────────────────────────────────────────────────────────────
85
+ export async function ingestFile(path, ctx, opts = {}) {
86
+ const abs = isAbsolute(path) ? path : resolve(process.cwd(), path);
87
+ if (!existsSync(abs)) {
88
+ throw new Error(`File not found: ${abs}`);
89
+ }
90
+ const stat = statSync(abs);
91
+ if (!stat.isFile()) {
92
+ throw new Error(`Not a file: ${abs}`);
93
+ }
94
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
95
+ const raw = readFileSync(abs, "utf-8");
96
+ const content = truncate(raw, maxBytes);
97
+ const title = basename(abs);
98
+ const created = createMemorySource({
99
+ kind: "file",
100
+ uri: abs,
101
+ title,
102
+ content,
103
+ scope: opts.scope,
104
+ tags: opts.tags,
105
+ });
106
+ if (!created)
107
+ return { ...sourceCreateFailure("file"), uri: abs, title };
108
+ const extracted = created.duplicate
109
+ ? []
110
+ : await maybeExtract(ctx, { kind: "file", id: created.id }, content, opts);
111
+ return {
112
+ sourceId: created.id,
113
+ duplicate: created.duplicate,
114
+ extracted,
115
+ kind: "file",
116
+ title,
117
+ uri: abs,
118
+ };
119
+ }
120
+ // ─── ingestUrl ──────────────────────────────────────────────────────────────
121
+ export async function ingestUrl(url, ctx, opts = {}) {
122
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
123
+ let body;
124
+ let title = null;
125
+ try {
126
+ const res = await fetch(url, { redirect: "follow" });
127
+ if (!res.ok)
128
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
129
+ body = await res.text();
130
+ const titleMatch = body.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
131
+ if (titleMatch)
132
+ title = titleMatch[1].trim().slice(0, 200);
133
+ }
134
+ catch (err) {
135
+ throw new Error(`Fetch failed for ${url}: ${err.message}`);
136
+ }
137
+ const content = truncate(stripHtml(body), maxBytes);
138
+ if (!content.trim()) {
139
+ throw new Error(`URL produced empty content: ${url}`);
140
+ }
141
+ const created = createMemorySource({
142
+ kind: "url",
143
+ uri: url,
144
+ title: title ?? url,
145
+ content,
146
+ scope: opts.scope,
147
+ tags: opts.tags,
148
+ });
149
+ if (!created)
150
+ return { ...sourceCreateFailure("url"), uri: url, title };
151
+ const extracted = created.duplicate
152
+ ? []
153
+ : await maybeExtract(ctx, { kind: "url", id: created.id }, content, opts);
154
+ return {
155
+ sourceId: created.id,
156
+ duplicate: created.duplicate,
157
+ extracted,
158
+ kind: "url",
159
+ title: title ?? url,
160
+ uri: url,
161
+ };
162
+ }
163
+ function stripHtml(html) {
164
+ return html
165
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
166
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
167
+ .replace(/<!--[\s\S]*?-->/g, " ")
168
+ .replace(/<[^>]+>/g, " ")
169
+ .replace(/\s+/g, " ")
170
+ .trim();
171
+ }
172
+ // ─── ingestArtifact ─────────────────────────────────────────────────────────
173
+ /**
174
+ * Ingest a named artifact from a milestone directory (e.g. LEARNINGS,
175
+ * SUMMARY, CONTEXT). Resolves through `resolveMilestoneFile` so worktree
176
+ * layouts are handled correctly.
177
+ */
178
+ export async function ingestArtifact(basePath, milestoneId, artifactType, ctx, opts = {}) {
179
+ const file = resolveMilestoneFile(basePath, milestoneId, artifactType);
180
+ if (!file || !existsSync(file)) {
181
+ throw new Error(`Artifact not found: ${milestoneId}-${artifactType}.md`);
182
+ }
183
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
184
+ const content = truncate(readFileSync(file, "utf-8"), maxBytes);
185
+ const title = `${milestoneId}-${artifactType}`;
186
+ const created = createMemorySource({
187
+ kind: "artifact",
188
+ uri: file,
189
+ title,
190
+ content,
191
+ scope: opts.scope,
192
+ tags: [...(opts.tags ?? []), milestoneId, artifactType.toLowerCase()],
193
+ });
194
+ if (!created)
195
+ return { ...sourceCreateFailure("artifact"), uri: file, title };
196
+ const extracted = created.duplicate
197
+ ? []
198
+ : await maybeExtract(ctx, { kind: "artifact", id: created.id }, content, opts);
199
+ return {
200
+ sourceId: created.id,
201
+ duplicate: created.duplicate,
202
+ extracted,
203
+ kind: "artifact",
204
+ title,
205
+ uri: file,
206
+ };
207
+ }
208
+ // ─── Helpers ────────────────────────────────────────────────────────────────
209
+ export function summarizeIngest(result) {
210
+ if (!result.sourceId)
211
+ return "Ingest failed: could not persist source.";
212
+ const status = result.duplicate ? "duplicate (content_hash match)" : "new source";
213
+ const extracted = result.extracted.length === 0
214
+ ? "no memories extracted"
215
+ : `${result.extracted.length} memor${result.extracted.length === 1 ? "y" : "ies"} applied`;
216
+ const label = result.title ? ` "${result.title}"` : "";
217
+ return `Ingested ${result.kind}${label} as ${result.sourceId} (${status}, ${extracted}).`;
218
+ }
@@ -0,0 +1,189 @@
1
+ // GSD Memory Relations — knowledge-graph edges between memories
2
+ //
3
+ // Phase 4 companion to memory-store.ts. Edges live in the `memory_relations`
4
+ // table and are created by (a) explicit LINK actions emitted by the memory
5
+ // extractor, or (b) future `/gsd memory link` CLI commands. All writes go
6
+ // through the single-writer gate in `gsd-db.ts`.
7
+ import { _getAdapter, deleteMemoryRelationsFor, insertMemoryRelationRow, isDbAvailable, } from "./gsd-db.js";
8
+ export const VALID_RELATIONS = [
9
+ "related_to",
10
+ "depends_on",
11
+ "contradicts",
12
+ "elaborates",
13
+ "supersedes",
14
+ ];
15
+ // ─── Helpers ────────────────────────────────────────────────────────────────
16
+ export function isValidRelation(value) {
17
+ return typeof value === "string" && VALID_RELATIONS.includes(value);
18
+ }
19
+ function clampConfidence(value) {
20
+ if (typeof value !== "number" || !Number.isFinite(value))
21
+ return 0.8;
22
+ if (value < 0.1)
23
+ return 0.1;
24
+ if (value > 0.99)
25
+ return 0.99;
26
+ return value;
27
+ }
28
+ // ─── Mutations ──────────────────────────────────────────────────────────────
29
+ export function createMemoryRelation(from, to, rel, confidence) {
30
+ if (!isDbAvailable())
31
+ return false;
32
+ if (!from || !to || from === to || !isValidRelation(rel))
33
+ return false;
34
+ const adapter = _getAdapter();
35
+ if (!adapter)
36
+ return false;
37
+ try {
38
+ // Verify both memories exist — relation rows without endpoints are useless.
39
+ const fromRow = adapter.prepare("SELECT 1 FROM memories WHERE id = :id").get({ ":id": from });
40
+ const toRow = adapter.prepare("SELECT 1 FROM memories WHERE id = :id").get({ ":id": to });
41
+ if (!fromRow || !toRow)
42
+ return false;
43
+ insertMemoryRelationRow({
44
+ fromId: from,
45
+ toId: to,
46
+ rel,
47
+ confidence: clampConfidence(confidence),
48
+ createdAt: new Date().toISOString(),
49
+ });
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ export function removeMemoryRelationsFor(memoryId) {
57
+ if (!isDbAvailable() || !memoryId)
58
+ return;
59
+ try {
60
+ deleteMemoryRelationsFor(memoryId);
61
+ }
62
+ catch {
63
+ // non-fatal
64
+ }
65
+ }
66
+ // ─── Queries ────────────────────────────────────────────────────────────────
67
+ export function listRelationsFor(memoryId) {
68
+ if (!isDbAvailable())
69
+ return [];
70
+ const adapter = _getAdapter();
71
+ if (!adapter)
72
+ return [];
73
+ try {
74
+ const rows = adapter
75
+ .prepare("SELECT from_id, to_id, rel, confidence, created_at FROM memory_relations WHERE from_id = :id OR to_id = :id")
76
+ .all({ ":id": memoryId });
77
+ return rows.map(rowToRelation);
78
+ }
79
+ catch {
80
+ return [];
81
+ }
82
+ }
83
+ export function traverseGraph(startId, depth) {
84
+ const emptyResult = { nodes: [], edges: [] };
85
+ if (!isDbAvailable() || !startId)
86
+ return emptyResult;
87
+ const adapter = _getAdapter();
88
+ if (!adapter)
89
+ return emptyResult;
90
+ const hop = Math.max(0, Math.min(5, Math.floor(depth || 0)));
91
+ try {
92
+ const visited = new Set();
93
+ const queue = [{ id: startId, hop: 0 }];
94
+ const nodes = new Map();
95
+ const edges = [];
96
+ while (queue.length > 0) {
97
+ const { id, hop: level } = queue.shift();
98
+ if (visited.has(id))
99
+ continue;
100
+ visited.add(id);
101
+ const nodeRow = adapter
102
+ .prepare("SELECT id, category, content, confidence, superseded_by FROM memories WHERE id = :id")
103
+ .get({ ":id": id });
104
+ if (!nodeRow)
105
+ continue;
106
+ nodes.set(id, {
107
+ id: nodeRow["id"],
108
+ category: nodeRow["category"],
109
+ content: nodeRow["content"],
110
+ confidence: nodeRow["confidence"],
111
+ });
112
+ // Include supersedes edges from the base table so old graphs remain
113
+ // connected even before the extractor starts emitting LINK actions.
114
+ const successor = nodeRow["superseded_by"];
115
+ if (successor && successor !== "CAP_EXCEEDED") {
116
+ edges.push({
117
+ from: id,
118
+ to: successor,
119
+ rel: "supersedes",
120
+ confidence: 1,
121
+ createdAt: "",
122
+ });
123
+ if (!visited.has(successor) && level < hop) {
124
+ queue.push({ id: successor, hop: level + 1 });
125
+ }
126
+ }
127
+ const predecessors = adapter
128
+ .prepare("SELECT id FROM memories WHERE superseded_by = :id")
129
+ .all({ ":id": id });
130
+ for (const pred of predecessors) {
131
+ const predId = pred["id"];
132
+ edges.push({ from: predId, to: id, rel: "supersedes", confidence: 1, createdAt: "" });
133
+ if (!visited.has(predId) && level < hop) {
134
+ queue.push({ id: predId, hop: level + 1 });
135
+ }
136
+ }
137
+ if (level >= hop)
138
+ continue;
139
+ const outgoing = adapter
140
+ .prepare("SELECT from_id, to_id, rel, confidence, created_at FROM memory_relations WHERE from_id = :id")
141
+ .all({ ":id": id });
142
+ for (const row of outgoing) {
143
+ const edge = rowToRelation(row);
144
+ edges.push(edge);
145
+ if (!visited.has(edge.to))
146
+ queue.push({ id: edge.to, hop: level + 1 });
147
+ }
148
+ const incoming = adapter
149
+ .prepare("SELECT from_id, to_id, rel, confidence, created_at FROM memory_relations WHERE to_id = :id")
150
+ .all({ ":id": id });
151
+ for (const row of incoming) {
152
+ const edge = rowToRelation(row);
153
+ edges.push(edge);
154
+ if (!visited.has(edge.from))
155
+ queue.push({ id: edge.from, hop: level + 1 });
156
+ }
157
+ }
158
+ return {
159
+ nodes: [...nodes.values()],
160
+ edges: dedupeEdges(edges),
161
+ };
162
+ }
163
+ catch {
164
+ return emptyResult;
165
+ }
166
+ }
167
+ function rowToRelation(row) {
168
+ const relRaw = row["rel"];
169
+ const rel = isValidRelation(relRaw) ? relRaw : "related_to";
170
+ return {
171
+ from: row["from_id"],
172
+ to: row["to_id"],
173
+ rel,
174
+ confidence: row["confidence"] ?? 0.8,
175
+ createdAt: row["created_at"] ?? "",
176
+ };
177
+ }
178
+ function dedupeEdges(edges) {
179
+ const seen = new Set();
180
+ const out = [];
181
+ for (const e of edges) {
182
+ const key = `${e.from}|${e.to}|${e.rel}`;
183
+ if (seen.has(key))
184
+ continue;
185
+ seen.add(key);
186
+ out.push(e);
187
+ }
188
+ return out;
189
+ }
@@ -0,0 +1,113 @@
1
+ // GSD Memory Sources — CRUD for raw ingested content (notes, files, URLs, artifacts)
2
+ //
3
+ // Distinct from `memories`: a `memory_source` row is the preserved raw input
4
+ // that an extractor may (or may not) distill into one or more memories.
5
+ // Storing the source makes ingestion idempotent (content_hash) and gives the
6
+ // user a way to trace a memory back to its origin.
7
+ import { createHash, randomUUID } from "node:crypto";
8
+ import { _getAdapter, isDbAvailable, insertMemorySourceRow, deleteMemorySourceRow } from "./gsd-db.js";
9
+ function rowToSource(row) {
10
+ const tagsRaw = typeof row["tags"] === "string" ? row["tags"] : "[]";
11
+ let tags = [];
12
+ try {
13
+ const parsed = JSON.parse(tagsRaw);
14
+ if (Array.isArray(parsed))
15
+ tags = parsed.filter((t) => typeof t === "string");
16
+ }
17
+ catch {
18
+ // leave tags empty
19
+ }
20
+ return {
21
+ id: row["id"],
22
+ kind: row["kind"],
23
+ uri: row["uri"] ?? null,
24
+ title: row["title"] ?? null,
25
+ content: row["content"],
26
+ content_hash: row["content_hash"],
27
+ imported_at: row["imported_at"],
28
+ scope: row["scope"] ?? "project",
29
+ tags,
30
+ };
31
+ }
32
+ export function hashContent(content) {
33
+ return createHash("sha256").update(content).digest("hex");
34
+ }
35
+ export function newSourceId() {
36
+ return `SRC-${randomUUID().slice(0, 8)}`;
37
+ }
38
+ /**
39
+ * Insert a memory_source. Idempotent — if the content_hash already exists,
40
+ * returns the existing source's id and duplicate=true instead of inserting.
41
+ */
42
+ export function createMemorySource(opts) {
43
+ if (!isDbAvailable())
44
+ return null;
45
+ const adapter = _getAdapter();
46
+ if (!adapter)
47
+ return null;
48
+ try {
49
+ const contentHash = hashContent(opts.content);
50
+ const existing = adapter
51
+ .prepare("SELECT id FROM memory_sources WHERE content_hash = :h")
52
+ .get({ ":h": contentHash });
53
+ if (existing && typeof existing["id"] === "string") {
54
+ return { id: existing["id"], duplicate: true };
55
+ }
56
+ const id = newSourceId();
57
+ insertMemorySourceRow({
58
+ id,
59
+ kind: opts.kind,
60
+ uri: opts.uri ?? null,
61
+ title: opts.title ?? null,
62
+ content: opts.content,
63
+ contentHash,
64
+ importedAt: new Date().toISOString(),
65
+ scope: opts.scope ?? "project",
66
+ tags: opts.tags ?? [],
67
+ });
68
+ return { id, duplicate: false };
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ export function listMemorySources(limit = 50) {
75
+ if (!isDbAvailable())
76
+ return [];
77
+ const adapter = _getAdapter();
78
+ if (!adapter)
79
+ return [];
80
+ try {
81
+ const rows = adapter
82
+ .prepare("SELECT * FROM memory_sources ORDER BY imported_at DESC LIMIT :limit")
83
+ .all({ ":limit": limit });
84
+ return rows.map(rowToSource);
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ }
90
+ export function getMemorySource(id) {
91
+ if (!isDbAvailable())
92
+ return null;
93
+ const adapter = _getAdapter();
94
+ if (!adapter)
95
+ return null;
96
+ try {
97
+ const row = adapter.prepare("SELECT * FROM memory_sources WHERE id = :id").get({ ":id": id });
98
+ return row ? rowToSource(row) : null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ export function deleteMemorySource(id) {
105
+ if (!isDbAvailable())
106
+ return false;
107
+ try {
108
+ return deleteMemorySourceRow(id);
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ }