@gmickel/gno 1.20.0 → 1.22.0

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 (55) hide show
  1. package/README.md +29 -5
  2. package/assets/skill/SKILL.md +46 -15
  3. package/package.json +2 -1
  4. package/spec/cli.md +144 -0
  5. package/spec/db/schema.sql +170 -0
  6. package/spec/evals-agentic.md +48 -0
  7. package/spec/mcp.md +22 -0
  8. package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
  9. package/spec/output-schemas/changes.schema.json +280 -0
  10. package/spec/output-schemas/document-diff.schema.json +185 -0
  11. package/spec/output-schemas/impact.schema.json +122 -0
  12. package/spec/output-schemas/publish-artifact.schema.json +284 -0
  13. package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
  14. package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
  15. package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
  16. package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
  17. package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
  18. package/src/cli/commands/changes.ts +160 -0
  19. package/src/cli/commands/context-saved.ts +189 -0
  20. package/src/cli/options.ts +8 -0
  21. package/src/cli/program.ts +195 -0
  22. package/src/core/capsule-registry.ts +279 -0
  23. package/src/core/capsule-reverification-scheduler.ts +218 -0
  24. package/src/core/capsule-reverification.ts +289 -0
  25. package/src/core/change-diff.ts +182 -0
  26. package/src/core/change-journal.ts +228 -0
  27. package/src/core/knowledge-delta.ts +395 -0
  28. package/src/core/knowledge-impact.ts +202 -0
  29. package/src/ingestion/sync.ts +214 -165
  30. package/src/mcp/tools/changes.ts +80 -0
  31. package/src/mcp/tools/index.ts +29 -0
  32. package/src/publish/artifact-validation.ts +259 -0
  33. package/src/publish/artifact.ts +234 -118
  34. package/src/publish/export-service.ts +5 -9
  35. package/src/publish/metadata.ts +195 -0
  36. package/src/sdk/client.ts +42 -0
  37. package/src/sdk/index.ts +7 -0
  38. package/src/sdk/types.ts +22 -0
  39. package/src/serve/doc-events.ts +12 -1
  40. package/src/serve/resident-runtime.ts +22 -0
  41. package/src/serve/routes/api.ts +13 -0
  42. package/src/serve/routes/changes.ts +102 -0
  43. package/src/serve/server.ts +34 -0
  44. package/src/serve/watch-service.ts +9 -0
  45. package/src/store/index.ts +21 -0
  46. package/src/store/migrations/015-document-change-journal.ts +85 -0
  47. package/src/store/migrations/016-saved-capsules.ts +131 -0
  48. package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
  49. package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
  50. package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
  51. package/src/store/migrations/index.ts +10 -0
  52. package/src/store/sqlite/adapter.ts +291 -7
  53. package/src/store/sqlite/capsule-registry-store.ts +534 -0
  54. package/src/store/sqlite/change-journal-store.ts +473 -0
  55. package/src/store/types.ts +262 -0
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Shared read services for retained knowledge changes, structural diffs, and
3
+ * bounded inbound dependency impact.
4
+ */
5
+
6
+ import type {
7
+ DocumentChangeRow,
8
+ DocumentChangeStructureDelta,
9
+ DocumentRow,
10
+ StorePort,
11
+ } from "../store/types";
12
+
13
+ import {
14
+ decodeDocumentChangeCursor,
15
+ encodeDocumentChangeCursor,
16
+ } from "./change-journal";
17
+ import { resolveDocRef } from "./ref-parser";
18
+
19
+ const DEFAULT_CHANGE_LIMIT = 100;
20
+ const MAX_CHANGE_LIMIT = 1000;
21
+ const MAX_DIFF_SCAN_PAGES = 10;
22
+ const DIFF_SCAN_PAGE_SIZE = 1000;
23
+
24
+ export interface KnowledgeChangeSnapshot {
25
+ relPath: string;
26
+ docid: string;
27
+ uri: string;
28
+ sourceHash: string;
29
+ mirrorHash: string | null;
30
+ active: boolean;
31
+ }
32
+
33
+ export interface KnowledgeChange {
34
+ id: string;
35
+ kind: DocumentChangeRow["kind"];
36
+ collection: string;
37
+ observedAt: string;
38
+ previous: KnowledgeChangeSnapshot | null;
39
+ current: KnowledgeChangeSnapshot | null;
40
+ structureDelta: DocumentChangeStructureDelta;
41
+ }
42
+
43
+ export interface KnowledgeChangesResult {
44
+ schemaVersion: "1.0";
45
+ changes: KnowledgeChange[];
46
+ page: {
47
+ nextCursor: string | null;
48
+ earliestCursor: string;
49
+ latestCursor: string;
50
+ cursorExpired: boolean;
51
+ truncated: boolean;
52
+ retentionTruncated: boolean;
53
+ };
54
+ warnings: string[];
55
+ }
56
+
57
+ export interface ListKnowledgeChangesInput {
58
+ since?: string;
59
+ collection?: string;
60
+ limit?: number;
61
+ }
62
+
63
+ export interface KnowledgeDocument {
64
+ id: string;
65
+ uri: string;
66
+ title: string | null;
67
+ collection: string;
68
+ relPath: string;
69
+ active?: boolean;
70
+ }
71
+
72
+ export interface KnowledgeDiffResult {
73
+ schemaVersion: "1.0";
74
+ status: "available" | "expired" | "unavailable";
75
+ document: KnowledgeDocument & { active: boolean };
76
+ change: KnowledgeChange | null;
77
+ content: {
78
+ status: "not_retained";
79
+ reason: "journal_metadata_only";
80
+ };
81
+ history: {
82
+ status: "available" | "partial" | "unavailable";
83
+ reason:
84
+ | null
85
+ | "structure_delta_truncated"
86
+ | "change_expired"
87
+ | "no_retained_change"
88
+ | "change_not_found";
89
+ };
90
+ warnings: string[];
91
+ }
92
+
93
+ export type KnowledgeDeltaServiceResult<T> =
94
+ | { success: true; data: T }
95
+ | { success: false; error: string; isValidation?: boolean };
96
+
97
+ const snapshot = (
98
+ row: DocumentChangeRow,
99
+ side: "old" | "new"
100
+ ): KnowledgeChangeSnapshot | null => {
101
+ const prefix = side === "old" ? "old" : "new";
102
+ const relPath = row[`${prefix}RelPath`];
103
+ const docid = row[`${prefix}Docid`];
104
+ const uri = row[`${prefix}Uri`];
105
+ const sourceHash = row[`${prefix}SourceHash`];
106
+ const active = row[`${prefix}Active`];
107
+ if (
108
+ relPath === null ||
109
+ docid === null ||
110
+ uri === null ||
111
+ sourceHash === null ||
112
+ active === null
113
+ ) {
114
+ return null;
115
+ }
116
+ return {
117
+ relPath,
118
+ docid,
119
+ uri,
120
+ sourceHash,
121
+ mirrorHash: row[`${prefix}MirrorHash`],
122
+ active,
123
+ };
124
+ };
125
+
126
+ export const projectKnowledgeChange = (
127
+ row: DocumentChangeRow
128
+ ): KnowledgeChange => ({
129
+ id: encodeDocumentChangeCursor(row.sequence),
130
+ kind: row.kind,
131
+ collection: row.collection,
132
+ observedAt: new Date(row.observedAtMs).toISOString(),
133
+ previous: snapshot(row, "old"),
134
+ current: snapshot(row, "new"),
135
+ structureDelta: row.structureDelta,
136
+ });
137
+
138
+ const parseSince = (
139
+ since: string | undefined
140
+ ): { cursor?: string; observedAfterMs?: number } | { error: string } => {
141
+ if (since === undefined) return {};
142
+ const trimmed = since.trim();
143
+ if (!trimmed) return { error: "since cannot be empty" };
144
+ try {
145
+ decodeDocumentChangeCursor(trimmed);
146
+ return { cursor: trimmed };
147
+ } catch {
148
+ const observedAfterMs = Date.parse(trimmed);
149
+ return Number.isFinite(observedAfterMs)
150
+ ? { observedAfterMs }
151
+ : { error: "since must be an ISO-8601 time or opaque change cursor" };
152
+ }
153
+ };
154
+
155
+ export async function listKnowledgeChanges(
156
+ store: StorePort,
157
+ input: ListKnowledgeChangesInput = {}
158
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeChangesResult>> {
159
+ if ((input.since?.length ?? 0) > 512) {
160
+ return {
161
+ success: false,
162
+ error: "since must be at most 512 characters",
163
+ isValidation: true,
164
+ };
165
+ }
166
+ const collection = input.collection?.trim();
167
+ if (
168
+ input.collection !== undefined &&
169
+ (!collection || collection.length > 256)
170
+ ) {
171
+ return {
172
+ success: false,
173
+ error: "collection must be between 1 and 256 characters",
174
+ isValidation: true,
175
+ };
176
+ }
177
+ const limit = input.limit ?? DEFAULT_CHANGE_LIMIT;
178
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CHANGE_LIMIT) {
179
+ return {
180
+ success: false,
181
+ error: `limit must be between 1 and ${MAX_CHANGE_LIMIT}`,
182
+ isValidation: true,
183
+ };
184
+ }
185
+ const since = parseSince(input.since);
186
+ if ("error" in since) {
187
+ return { success: false, error: since.error, isValidation: true };
188
+ }
189
+ const listed = await store.listDocumentChanges({
190
+ ...since,
191
+ collection,
192
+ limit,
193
+ });
194
+ if (!listed.ok) {
195
+ return {
196
+ success: false,
197
+ error: listed.error.message,
198
+ isValidation: listed.error.code === "INVALID_INPUT",
199
+ };
200
+ }
201
+ const retentionTruncated =
202
+ decodeDocumentChangeCursor(listed.value.earliestCursor) > 0;
203
+ const warnings: string[] = [];
204
+ if (listed.value.cursorExpired) {
205
+ warnings.push(
206
+ `Requested cursor expired; resume from ${listed.value.earliestCursor}`
207
+ );
208
+ } else if (retentionTruncated && !since.cursor) {
209
+ warnings.push("Earlier journal history was removed by retention");
210
+ }
211
+ return {
212
+ success: true,
213
+ data: {
214
+ schemaVersion: "1.0",
215
+ changes: listed.value.changes.map(projectKnowledgeChange),
216
+ page: {
217
+ nextCursor: listed.value.nextCursor,
218
+ earliestCursor: listed.value.earliestCursor,
219
+ latestCursor: listed.value.latestCursor,
220
+ cursorExpired: listed.value.cursorExpired,
221
+ truncated: listed.value.truncated,
222
+ retentionTruncated,
223
+ },
224
+ warnings,
225
+ },
226
+ };
227
+ }
228
+
229
+ const document = (
230
+ row: DocumentRow
231
+ ): KnowledgeDocument & { active: boolean } => ({
232
+ id: row.docid,
233
+ uri: row.uri,
234
+ title: row.title,
235
+ collection: row.collection,
236
+ relPath: row.relPath,
237
+ active: row.active,
238
+ });
239
+
240
+ async function latestRetainedChange(
241
+ store: StorePort,
242
+ documentId: number
243
+ ): Promise<
244
+ | { ok: true; row: DocumentChangeRow | null; retentionTruncated: boolean }
245
+ | { ok: false; error: string }
246
+ > {
247
+ let cursor: string | undefined;
248
+ let latest: DocumentChangeRow | null = null;
249
+ let retentionTruncated = false;
250
+ for (let pageIndex = 0; pageIndex < MAX_DIFF_SCAN_PAGES; pageIndex += 1) {
251
+ const page = await store.listDocumentChanges({
252
+ cursor,
253
+ documentId,
254
+ limit: DIFF_SCAN_PAGE_SIZE,
255
+ });
256
+ if (!page.ok) return { ok: false, error: page.error.message };
257
+ retentionTruncated =
258
+ decodeDocumentChangeCursor(page.value.earliestCursor) > 0;
259
+ latest = page.value.changes.at(-1) ?? latest;
260
+ if (!page.value.nextCursor) {
261
+ return { ok: true, row: latest, retentionTruncated };
262
+ }
263
+ cursor = page.value.nextCursor;
264
+ }
265
+ return { ok: false, error: "Retained change scan exceeded its bound" };
266
+ }
267
+
268
+ async function exactRetainedChange(
269
+ store: StorePort,
270
+ documentId: number,
271
+ changeId: string
272
+ ): Promise<
273
+ | { ok: true; row: DocumentChangeRow | null; expired: boolean }
274
+ | { ok: false; error: string; isValidation?: boolean }
275
+ > {
276
+ let sequence: number;
277
+ try {
278
+ sequence = decodeDocumentChangeCursor(changeId);
279
+ } catch {
280
+ return { ok: false, error: "Invalid change id", isValidation: true };
281
+ }
282
+ if (sequence < 1) {
283
+ return { ok: false, error: "Invalid change id", isValidation: true };
284
+ }
285
+ const page = await store.listDocumentChanges({
286
+ cursor: encodeDocumentChangeCursor(sequence - 1),
287
+ documentId,
288
+ limit: 1,
289
+ });
290
+ if (!page.ok) {
291
+ return {
292
+ ok: false,
293
+ error: page.error.message,
294
+ isValidation: page.error.code === "INVALID_INPUT",
295
+ };
296
+ }
297
+ const row = page.value.changes[0] ?? null;
298
+ return {
299
+ ok: true,
300
+ row: row?.sequence === sequence ? row : null,
301
+ expired: page.value.cursorExpired,
302
+ };
303
+ }
304
+
305
+ export async function getKnowledgeDiff(
306
+ store: StorePort,
307
+ ref: string,
308
+ changeId?: string
309
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeDiffResult>> {
310
+ const normalizedRef = ref.trim();
311
+ if (!normalizedRef || normalizedRef.length > 4096) {
312
+ return {
313
+ success: false,
314
+ error: "ref must be between 1 and 4096 characters",
315
+ isValidation: true,
316
+ };
317
+ }
318
+ const normalizedChangeId = changeId?.trim();
319
+ if (
320
+ changeId !== undefined &&
321
+ (!normalizedChangeId || normalizedChangeId.length > 512)
322
+ ) {
323
+ return {
324
+ success: false,
325
+ error: "changeId must be between 1 and 512 characters",
326
+ isValidation: true,
327
+ };
328
+ }
329
+ const resolved = await resolveDocRef(store, normalizedRef);
330
+ if ("error" in resolved) {
331
+ return {
332
+ success: false,
333
+ error: resolved.error,
334
+ isValidation: resolved.isValidation,
335
+ };
336
+ }
337
+ let row: DocumentChangeRow | null;
338
+ let status: KnowledgeDiffResult["status"];
339
+ let history: KnowledgeDiffResult["history"];
340
+ const warnings = ["Source bodies are not retained in the change journal"];
341
+ if (normalizedChangeId) {
342
+ const exact = await exactRetainedChange(
343
+ store,
344
+ resolved.doc.id,
345
+ normalizedChangeId
346
+ );
347
+ if (!exact.ok) return { success: false, ...exact };
348
+ row = exact.row;
349
+ status = exact.expired ? "expired" : row ? "available" : "unavailable";
350
+ history = exact.expired
351
+ ? { status: "unavailable", reason: "change_expired" }
352
+ : row
353
+ ? row.structureDelta.truncated
354
+ ? { status: "partial", reason: "structure_delta_truncated" }
355
+ : { status: "available", reason: null }
356
+ : { status: "unavailable", reason: "change_not_found" };
357
+ } else {
358
+ const latest = await latestRetainedChange(store, resolved.doc.id);
359
+ if (!latest.ok) {
360
+ return { success: false, error: latest.error };
361
+ }
362
+ row = latest.row;
363
+ status = row ? "available" : "unavailable";
364
+ history = row
365
+ ? row.structureDelta.truncated
366
+ ? { status: "partial", reason: "structure_delta_truncated" }
367
+ : { status: "available", reason: null }
368
+ : { status: "unavailable", reason: "no_retained_change" };
369
+ if (latest.retentionTruncated) {
370
+ warnings.push("Earlier journal history was removed by retention");
371
+ }
372
+ }
373
+ return {
374
+ success: true,
375
+ data: {
376
+ schemaVersion: "1.0",
377
+ status,
378
+ document: document(resolved.doc),
379
+ change: row ? projectKnowledgeChange(row) : null,
380
+ content: {
381
+ status: "not_retained",
382
+ reason: "journal_metadata_only",
383
+ },
384
+ history,
385
+ warnings,
386
+ },
387
+ };
388
+ }
389
+
390
+ export { analyzeKnowledgeImpact } from "./knowledge-impact";
391
+ export type {
392
+ KnowledgeImpactEvidenceStep,
393
+ KnowledgeImpactInput,
394
+ KnowledgeImpactResult,
395
+ } from "./knowledge-impact";
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Bounded, cycle-safe inbound dependency impact analysis.
3
+ */
4
+
5
+ import type { DocEdgeRow, DocumentRow, StorePort } from "../store/types";
6
+ import type {
7
+ KnowledgeDeltaServiceResult,
8
+ KnowledgeDocument,
9
+ } from "./knowledge-delta";
10
+
11
+ import { resolveDocRef } from "./ref-parser";
12
+
13
+ export interface KnowledgeImpactEvidenceStep {
14
+ source: Pick<KnowledgeDocument, "id" | "uri">;
15
+ target: Pick<KnowledgeDocument, "id" | "uri">;
16
+ edgeType: string;
17
+ relationType: string;
18
+ confidence: DocEdgeRow["confidence"];
19
+ edgeSource: DocEdgeRow["edgeSource"];
20
+ }
21
+
22
+ export interface KnowledgeImpactResult {
23
+ schemaVersion: "1.0";
24
+ root: KnowledgeDocument;
25
+ impacted: Array<{
26
+ document: KnowledgeDocument;
27
+ depth: number;
28
+ evidencePath: KnowledgeImpactEvidenceStep[];
29
+ }>;
30
+ meta: {
31
+ maxDepth: number;
32
+ maxNodes: number;
33
+ maxEdges: number;
34
+ frontierLimit: number;
35
+ visitedLimit: number;
36
+ returnedNodes: number;
37
+ returnedEdges: number;
38
+ truncated: boolean;
39
+ warnings: string[];
40
+ };
41
+ }
42
+
43
+ export interface KnowledgeImpactInput {
44
+ maxDepth?: number;
45
+ maxNodes?: number;
46
+ maxEdges?: number;
47
+ frontierLimit?: number;
48
+ visitedLimit?: number;
49
+ }
50
+
51
+ const document = (row: DocumentRow): KnowledgeDocument => ({
52
+ id: row.docid,
53
+ uri: row.uri,
54
+ title: row.title,
55
+ collection: row.collection,
56
+ relPath: row.relPath,
57
+ });
58
+
59
+ const bounded = (
60
+ name: string,
61
+ value: number | undefined,
62
+ fallback: number,
63
+ maximum: number
64
+ ): number | { error: string } => {
65
+ const resolved = value ?? fallback;
66
+ return Number.isSafeInteger(resolved) && resolved >= 1 && resolved <= maximum
67
+ ? resolved
68
+ : { error: `${name} must be between 1 and ${maximum}` };
69
+ };
70
+
71
+ const edgeStep = (edge: DocEdgeRow): KnowledgeImpactEvidenceStep => ({
72
+ source: { id: edge.sourceDocid, uri: edge.sourceUri },
73
+ target: { id: edge.targetDocid, uri: edge.targetUri },
74
+ edgeType: edge.edgeType,
75
+ relationType: edge.relationType,
76
+ confidence: edge.confidence,
77
+ edgeSource: edge.edgeSource,
78
+ });
79
+
80
+ export async function analyzeKnowledgeImpact(
81
+ store: StorePort,
82
+ ref: string,
83
+ input: KnowledgeImpactInput = {}
84
+ ): Promise<KnowledgeDeltaServiceResult<KnowledgeImpactResult>> {
85
+ if (!ref.trim() || ref.length > 4096) {
86
+ return {
87
+ success: false,
88
+ error: "ref must be between 1 and 4096 characters",
89
+ isValidation: true,
90
+ };
91
+ }
92
+ const caps = {
93
+ maxDepth: bounded("maxDepth", input.maxDepth, 3, 6),
94
+ maxNodes: bounded("maxNodes", input.maxNodes, 100, 1000),
95
+ maxEdges: bounded("maxEdges", input.maxEdges, 250, 5000),
96
+ frontierLimit: bounded("frontierLimit", input.frontierLimit, 100, 1000),
97
+ visitedLimit: bounded("visitedLimit", input.visitedLimit, 500, 5000),
98
+ };
99
+ const invalid = Object.values(caps).find(
100
+ (value): value is { error: string } => typeof value === "object"
101
+ );
102
+ if (invalid) {
103
+ return { success: false, error: invalid.error, isValidation: true };
104
+ }
105
+ const values = caps as Record<keyof typeof caps, number>;
106
+ const resolved = await resolveDocRef(store, ref);
107
+ if ("error" in resolved) {
108
+ return {
109
+ success: false,
110
+ error: resolved.error,
111
+ isValidation: resolved.isValidation,
112
+ };
113
+ }
114
+ if (!resolved.doc.active) {
115
+ return {
116
+ success: false,
117
+ error: `Document is inactive: ${ref}`,
118
+ isValidation: true,
119
+ };
120
+ }
121
+ const traversal = await store.queryGraphTraversal(resolved.doc.id, {
122
+ direction: "in",
123
+ maxDepth: values.maxDepth,
124
+ maxNodes: values.maxNodes,
125
+ frontierLimit: values.frontierLimit,
126
+ visitedLimit: values.visitedLimit,
127
+ });
128
+ if (!traversal.ok) {
129
+ return { success: false, error: traversal.error.message };
130
+ }
131
+ const sortedEdges = [...traversal.value.edges]
132
+ .sort(
133
+ (a, b) =>
134
+ a.depth - b.depth ||
135
+ a.edge.edgeType.localeCompare(b.edge.edgeType) ||
136
+ a.edge.sourceUri.localeCompare(b.edge.sourceUri) ||
137
+ a.edge.targetUri.localeCompare(b.edge.targetUri)
138
+ )
139
+ .slice(0, values.maxEdges);
140
+ const incoming = new Map<string, DocEdgeRow[]>();
141
+ for (const { edge } of sortedEdges) {
142
+ const entries = incoming.get(edge.targetDocid) ?? [];
143
+ entries.push(edge);
144
+ incoming.set(edge.targetDocid, entries);
145
+ }
146
+ const paths = new Map<string, KnowledgeImpactEvidenceStep[]>([
147
+ [resolved.doc.docid, []],
148
+ ]);
149
+ const queue = [resolved.doc.docid];
150
+ for (let index = 0; index < queue.length; index += 1) {
151
+ const targetId = queue[index]!;
152
+ const targetPath = paths.get(targetId) ?? [];
153
+ for (const edge of incoming.get(targetId) ?? []) {
154
+ if (paths.has(edge.sourceDocid)) continue;
155
+ paths.set(edge.sourceDocid, [edgeStep(edge), ...targetPath]);
156
+ queue.push(edge.sourceDocid);
157
+ }
158
+ }
159
+ const nodesById = new Map(
160
+ traversal.value.nodes.map(({ doc: row }) => [row.docid, document(row)])
161
+ );
162
+ const impacted = [...paths.entries()]
163
+ .filter(([id]) => id !== resolved.doc.docid)
164
+ .map(([id, evidencePath]) => ({
165
+ document: nodesById.get(id)!,
166
+ depth: evidencePath.length,
167
+ evidencePath,
168
+ }))
169
+ .filter((item) => item.document)
170
+ .sort(
171
+ (a, b) =>
172
+ a.depth - b.depth || a.document.uri.localeCompare(b.document.uri)
173
+ );
174
+ const warnings = [...traversal.value.warnings];
175
+ if (traversal.value.edges.length > values.maxEdges) {
176
+ warnings.push("maxEdges reached");
177
+ }
178
+ const usedEdges = new Set(
179
+ impacted.flatMap(({ evidencePath }) =>
180
+ evidencePath.map(
181
+ (step) =>
182
+ `${step.source.id}\u0000${step.target.id}\u0000${step.edgeType}`
183
+ )
184
+ )
185
+ );
186
+ return {
187
+ success: true,
188
+ data: {
189
+ schemaVersion: "1.0",
190
+ root: document(resolved.doc),
191
+ impacted,
192
+ meta: {
193
+ ...values,
194
+ returnedNodes: impacted.length + 1,
195
+ returnedEdges: usedEdges.size,
196
+ truncated:
197
+ traversal.value.truncated || warnings.includes("maxEdges reached"),
198
+ warnings: [...new Set(warnings)],
199
+ },
200
+ },
201
+ };
202
+ }