@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.
- package/README.md +29 -5
- package/assets/skill/SKILL.md +46 -15
- package/package.json +2 -1
- package/spec/cli.md +144 -0
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +48 -0
- package/spec/mcp.md +22 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/publish-artifact.schema.json +284 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +195 -0
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/ingestion/sync.ts +214 -165
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/index.ts +29 -0
- package/src/publish/artifact-validation.ts +259 -0
- package/src/publish/artifact.ts +234 -118
- package/src/publish/export-service.ts +5 -9
- package/src/publish/metadata.ts +195 -0
- package/src/sdk/client.ts +42 -0
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +13 -0
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- 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
|
+
}
|