@gmickel/gno 1.20.0 → 1.21.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 +19 -4
- package/assets/skill/SKILL.md +46 -15
- package/package.json +1 -1
- package/spec/cli.md +100 -0
- package/spec/db/schema.sql +170 -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/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/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,80 @@
|
|
|
1
|
+
/** Read-only MCP adapters for knowledge change, diff, and impact services. */
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { ToolContext } from "../server";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
analyzeKnowledgeImpact,
|
|
9
|
+
getKnowledgeDiff,
|
|
10
|
+
listKnowledgeChanges,
|
|
11
|
+
} from "../../core/knowledge-delta";
|
|
12
|
+
import { runTool, type ToolResult } from "./index";
|
|
13
|
+
|
|
14
|
+
export const changesInputSchema = z.object({
|
|
15
|
+
since: z.string().trim().min(1).max(512).optional(),
|
|
16
|
+
collection: z.string().trim().min(1).max(256).optional(),
|
|
17
|
+
limit: z.number().int().min(1).max(1000).default(100),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const diffInputSchema = z.object({
|
|
21
|
+
ref: z.string().trim().min(1).max(4096),
|
|
22
|
+
change: z.string().trim().min(1).max(512).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const impactInputSchema = z.object({
|
|
26
|
+
ref: z.string().trim().min(1).max(4096),
|
|
27
|
+
maxDepth: z.number().int().min(1).max(6).default(3),
|
|
28
|
+
maxNodes: z.number().int().min(1).max(1000).default(100),
|
|
29
|
+
maxEdges: z.number().int().min(1).max(5000).default(250),
|
|
30
|
+
frontierLimit: z.number().int().min(1).max(1000).default(100),
|
|
31
|
+
visitedLimit: z.number().int().min(1).max(5000).default(500),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const unwrap = <T>(
|
|
35
|
+
result:
|
|
36
|
+
| { success: true; data: T }
|
|
37
|
+
| { success: false; error: string; isValidation?: boolean }
|
|
38
|
+
): T => {
|
|
39
|
+
if (result.success) return result.data;
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${result.isValidation ? "VALIDATION" : "RUNTIME"}: ${result.error}`
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const handleChanges = (
|
|
46
|
+
args: z.infer<typeof changesInputSchema>,
|
|
47
|
+
ctx: ToolContext
|
|
48
|
+
): Promise<ToolResult> =>
|
|
49
|
+
runTool(
|
|
50
|
+
ctx,
|
|
51
|
+
"gno_changes",
|
|
52
|
+
async () => unwrap(await listKnowledgeChanges(ctx.store, args)),
|
|
53
|
+
(data) =>
|
|
54
|
+
`${data.changes.length} retained document changes${data.page.truncated ? " (more available)" : ""}`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const handleDiff = (
|
|
58
|
+
args: z.infer<typeof diffInputSchema>,
|
|
59
|
+
ctx: ToolContext
|
|
60
|
+
): Promise<ToolResult> =>
|
|
61
|
+
runTool(
|
|
62
|
+
ctx,
|
|
63
|
+
"gno_diff",
|
|
64
|
+
async () =>
|
|
65
|
+
unwrap(await getKnowledgeDiff(ctx.store, args.ref, args.change)),
|
|
66
|
+
(data) =>
|
|
67
|
+
`Structural diff for ${data.document.uri}: ${data.status}; history ${data.history.status}; source bodies not retained`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const handleImpact = (
|
|
71
|
+
args: z.infer<typeof impactInputSchema>,
|
|
72
|
+
ctx: ToolContext
|
|
73
|
+
): Promise<ToolResult> =>
|
|
74
|
+
runTool(
|
|
75
|
+
ctx,
|
|
76
|
+
"gno_impact",
|
|
77
|
+
async () => unwrap(await analyzeKnowledgeImpact(ctx.store, args.ref, args)),
|
|
78
|
+
(data) =>
|
|
79
|
+
`${data.impacted.length} documents depend on ${data.root.uri}${data.meta.truncated ? " (truncated)" : ""}`
|
|
80
|
+
);
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -21,6 +21,14 @@ import { normalizeTag } from "../../core/tags";
|
|
|
21
21
|
import { handleAddCollection } from "./add-collection";
|
|
22
22
|
import { askInputSchema, handleAsk } from "./ask";
|
|
23
23
|
import { handleCapture } from "./capture";
|
|
24
|
+
import {
|
|
25
|
+
changesInputSchema,
|
|
26
|
+
diffInputSchema,
|
|
27
|
+
handleChanges,
|
|
28
|
+
handleDiff,
|
|
29
|
+
handleImpact,
|
|
30
|
+
impactInputSchema,
|
|
31
|
+
} from "./changes";
|
|
24
32
|
import { handleClearCollectionEmbeddings } from "./clear-collection-embeddings";
|
|
25
33
|
import { handleContext, handleContextVerify } from "./context";
|
|
26
34
|
import { handleEmbed } from "./embed";
|
|
@@ -1036,6 +1044,27 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1036
1044
|
(args) => handleStatus(args, ctx)
|
|
1037
1045
|
);
|
|
1038
1046
|
|
|
1047
|
+
server.tool(
|
|
1048
|
+
"gno_changes",
|
|
1049
|
+
"List retained metadata-only document changes with opaque cursor pagination and retention disclosure.",
|
|
1050
|
+
changesInputSchema.shape,
|
|
1051
|
+
(args) => handleChanges(args, ctx)
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
server.tool(
|
|
1055
|
+
"gno_diff",
|
|
1056
|
+
"Inspect one retained metadata-only structural document change. Source bodies are never returned.",
|
|
1057
|
+
diffInputSchema.shape,
|
|
1058
|
+
(args) => handleDiff(args, ctx)
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
server.tool(
|
|
1062
|
+
"gno_impact",
|
|
1063
|
+
"Find bounded inbound typed, wiki, and Markdown dependencies with deterministic evidence paths.",
|
|
1064
|
+
impactInputSchema.shape,
|
|
1065
|
+
(args) => handleImpact(args, ctx)
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1039
1068
|
server.tool(
|
|
1040
1069
|
"gno_trace_list",
|
|
1041
1070
|
"List bounded metadata-only summaries of private local retrieval traces. Raw replay queries are omitted from history.",
|
package/src/sdk/client.ts
CHANGED
|
@@ -40,6 +40,11 @@ import type {
|
|
|
40
40
|
GnoRenameNoteOptions,
|
|
41
41
|
GnoUpdateOptions,
|
|
42
42
|
GnoVectorSearchOptions,
|
|
43
|
+
KnowledgeChangesResult,
|
|
44
|
+
KnowledgeDiffResult,
|
|
45
|
+
KnowledgeImpactInput,
|
|
46
|
+
KnowledgeImpactResult,
|
|
47
|
+
ListKnowledgeChangesInput,
|
|
43
48
|
} from "./types";
|
|
44
49
|
|
|
45
50
|
import {
|
|
@@ -84,6 +89,12 @@ import {
|
|
|
84
89
|
planRenameRefactor,
|
|
85
90
|
} from "../core/file-refactors";
|
|
86
91
|
import { resolveEffectiveIndex } from "../core/indexed-reference";
|
|
92
|
+
import {
|
|
93
|
+
analyzeKnowledgeImpact,
|
|
94
|
+
getKnowledgeDiff,
|
|
95
|
+
listKnowledgeChanges,
|
|
96
|
+
type KnowledgeDeltaServiceResult,
|
|
97
|
+
} from "../core/knowledge-delta";
|
|
87
98
|
import { resolveNoteCreatePlan } from "../core/note-creation";
|
|
88
99
|
import { resolveNotePreset } from "../core/note-presets";
|
|
89
100
|
import { RetrievalTraceManagementService } from "../core/retrieval-trace-management";
|
|
@@ -174,6 +185,11 @@ function unwrapTraceStore<T>(result: StoreResult<T>): T {
|
|
|
174
185
|
});
|
|
175
186
|
}
|
|
176
187
|
|
|
188
|
+
function unwrapKnowledgeDelta<T>(result: KnowledgeDeltaServiceResult<T>): T {
|
|
189
|
+
if (result.success) return result.data;
|
|
190
|
+
throw sdkError(result.isValidation ? "VALIDATION" : "STORE", result.error);
|
|
191
|
+
}
|
|
192
|
+
|
|
177
193
|
async function resolveClientState(
|
|
178
194
|
options: GnoClientInitOptions = {}
|
|
179
195
|
): Promise<OpenedClientState> {
|
|
@@ -1067,6 +1083,32 @@ class GnoClientImpl implements GnoClient {
|
|
|
1067
1083
|
};
|
|
1068
1084
|
}
|
|
1069
1085
|
|
|
1086
|
+
async changes(
|
|
1087
|
+
options: ListKnowledgeChangesInput = {}
|
|
1088
|
+
): Promise<KnowledgeChangesResult> {
|
|
1089
|
+
this.assertOpen();
|
|
1090
|
+
return unwrapKnowledgeDelta(
|
|
1091
|
+
await listKnowledgeChanges(this.store, options)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async diff(ref: string, changeId?: string): Promise<KnowledgeDiffResult> {
|
|
1096
|
+
this.assertOpen();
|
|
1097
|
+
return unwrapKnowledgeDelta(
|
|
1098
|
+
await getKnowledgeDiff(this.store, ref, changeId)
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async impact(
|
|
1103
|
+
ref: string,
|
|
1104
|
+
options: KnowledgeImpactInput = {}
|
|
1105
|
+
): Promise<KnowledgeImpactResult> {
|
|
1106
|
+
this.assertOpen();
|
|
1107
|
+
return unwrapKnowledgeDelta(
|
|
1108
|
+
await analyzeKnowledgeImpact(this.store, ref, options)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1070
1112
|
async status(): Promise<IndexStatus> {
|
|
1071
1113
|
this.assertOpen();
|
|
1072
1114
|
return unwrapStore(
|
package/src/sdk/index.ts
CHANGED
|
@@ -54,6 +54,13 @@ export type {
|
|
|
54
54
|
GnoSkippedDocument,
|
|
55
55
|
GnoUpdateOptions,
|
|
56
56
|
GnoVectorSearchOptions,
|
|
57
|
+
KnowledgeChange,
|
|
58
|
+
KnowledgeChangesResult,
|
|
59
|
+
KnowledgeDiffResult,
|
|
60
|
+
KnowledgeImpactEvidenceStep,
|
|
61
|
+
KnowledgeImpactInput,
|
|
62
|
+
KnowledgeImpactResult,
|
|
63
|
+
ListKnowledgeChangesInput,
|
|
57
64
|
} from "./types";
|
|
58
65
|
export {
|
|
59
66
|
ContextCapsuleContractError,
|
package/src/sdk/types.ts
CHANGED
|
@@ -17,6 +17,13 @@ import type {
|
|
|
17
17
|
} from "../core/context-capsule";
|
|
18
18
|
import type { ContextEvidenceErrorCode } from "../core/context-evidence";
|
|
19
19
|
import type { ContextVerifierErrorCode } from "../core/context-verifier";
|
|
20
|
+
import type {
|
|
21
|
+
KnowledgeChangesResult,
|
|
22
|
+
KnowledgeDiffResult,
|
|
23
|
+
KnowledgeImpactInput,
|
|
24
|
+
KnowledgeImpactResult,
|
|
25
|
+
ListKnowledgeChangesInput,
|
|
26
|
+
} from "../core/knowledge-delta";
|
|
20
27
|
import type { NoteCollisionPolicy } from "../core/note-creation";
|
|
21
28
|
import type { NotePresetId } from "../core/note-presets";
|
|
22
29
|
import type {
|
|
@@ -52,6 +59,15 @@ export type {
|
|
|
52
59
|
SyncResult,
|
|
53
60
|
};
|
|
54
61
|
export type { AskOptions, HybridSearchOptions } from "../pipeline/types";
|
|
62
|
+
export type {
|
|
63
|
+
KnowledgeChange,
|
|
64
|
+
KnowledgeChangesResult,
|
|
65
|
+
KnowledgeDiffResult,
|
|
66
|
+
KnowledgeImpactEvidenceStep,
|
|
67
|
+
KnowledgeImpactInput,
|
|
68
|
+
KnowledgeImpactResult,
|
|
69
|
+
ListKnowledgeChangesInput,
|
|
70
|
+
} from "../core/knowledge-delta";
|
|
55
71
|
export type { Collection, Config as GnoConfig, Context } from "../config/types";
|
|
56
72
|
export type { GetResponse as GnoGetResult } from "../cli/commands/get";
|
|
57
73
|
export type {
|
|
@@ -233,6 +249,12 @@ export interface GnoClient {
|
|
|
233
249
|
list(
|
|
234
250
|
options?: GnoListOptions
|
|
235
251
|
): Promise<import("../cli/commands/ls").LsResponse>;
|
|
252
|
+
changes(options?: ListKnowledgeChangesInput): Promise<KnowledgeChangesResult>;
|
|
253
|
+
diff(ref: string, changeId?: string): Promise<KnowledgeDiffResult>;
|
|
254
|
+
impact(
|
|
255
|
+
ref: string,
|
|
256
|
+
options?: KnowledgeImpactInput
|
|
257
|
+
): Promise<KnowledgeImpactResult>;
|
|
236
258
|
status(): Promise<IndexStatus>;
|
|
237
259
|
listRetrievalTraces(
|
|
238
260
|
options?: RetrievalTraceListRequest
|
package/src/serve/doc-events.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type DocumentEventOrigin = "watcher" | "save" | "create";
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface DocumentChangedEvent {
|
|
4
4
|
type: "document-changed";
|
|
5
5
|
uri: string;
|
|
6
6
|
collection: string;
|
|
@@ -9,6 +9,17 @@ export interface DocumentEvent {
|
|
|
9
9
|
changedAt: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface CapsuleReverifiedEvent {
|
|
13
|
+
type: "capsule-reverified";
|
|
14
|
+
registrationId: string;
|
|
15
|
+
capsuleId: string;
|
|
16
|
+
operationStatus: "completed" | "failed";
|
|
17
|
+
affectedQuestionState: "unaffected" | "affected" | "unknown";
|
|
18
|
+
changedAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DocumentEvent = DocumentChangedEvent | CapsuleReverifiedEvent;
|
|
22
|
+
|
|
12
23
|
export interface DocumentEventBusState {
|
|
13
24
|
connectedClients: number;
|
|
14
25
|
retryMs: number;
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
isInitialized,
|
|
32
32
|
loadConfig,
|
|
33
33
|
} from "../config";
|
|
34
|
+
import { SavedCapsuleReverificationScheduler } from "../core/capsule-reverification-scheduler";
|
|
34
35
|
import { acquireWriteLock } from "../core/file-lock";
|
|
35
36
|
import { JobManager } from "../core/job-manager";
|
|
36
37
|
import { recordContentMutation } from "../core/mutation-generations";
|
|
@@ -94,6 +95,7 @@ export interface ResidentRuntime {
|
|
|
94
95
|
readonly toolMutex: Mutex;
|
|
95
96
|
readonly readerGate: ReaderGate;
|
|
96
97
|
readonly jobManager: JobManager;
|
|
98
|
+
readonly capsuleReverificationScheduler: SavedCapsuleReverificationScheduler;
|
|
97
99
|
readonly modelManager: ModelManager;
|
|
98
100
|
readonly mcpContext: ToolContext;
|
|
99
101
|
readonly generations: ResidentGeneration;
|
|
@@ -266,6 +268,8 @@ export async function startResidentRuntime(
|
|
|
266
268
|
ctxHolder.current.scheduler = scheduler;
|
|
267
269
|
ctxHolder.current.eventBus = options.eventBus ?? null;
|
|
268
270
|
|
|
271
|
+
let capsuleReverificationScheduler: SavedCapsuleReverificationScheduler | null =
|
|
272
|
+
null;
|
|
269
273
|
const watchService = (
|
|
270
274
|
deps.watchServiceFactory ??
|
|
271
275
|
((watchOptions) => new DefaultCollectionWatchService(watchOptions))
|
|
@@ -282,6 +286,10 @@ export async function startResidentRuntime(
|
|
|
282
286
|
});
|
|
283
287
|
options.watchCallbacks?.onSyncComplete?.(event);
|
|
284
288
|
},
|
|
289
|
+
onSettled: () => {
|
|
290
|
+
capsuleReverificationScheduler?.notifySyncSettled();
|
|
291
|
+
options.watchCallbacks?.onSettled?.();
|
|
292
|
+
},
|
|
285
293
|
},
|
|
286
294
|
syncOptions: withContentTypeRules({}, initialConfig),
|
|
287
295
|
});
|
|
@@ -312,6 +320,17 @@ export async function startResidentRuntime(
|
|
|
312
320
|
const backgroundWork = new ResidentBackgroundWork(
|
|
313
321
|
() => !disposed && admission.accepting
|
|
314
322
|
);
|
|
323
|
+
capsuleReverificationScheduler = new SavedCapsuleReverificationScheduler({
|
|
324
|
+
deps: {
|
|
325
|
+
store,
|
|
326
|
+
get config() {
|
|
327
|
+
return ctxHolder.config;
|
|
328
|
+
},
|
|
329
|
+
indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME),
|
|
330
|
+
notify: (event) => options.eventBus?.emit(event),
|
|
331
|
+
},
|
|
332
|
+
startBackgroundWork: (operation) => backgroundWork.start(operation),
|
|
333
|
+
});
|
|
315
334
|
|
|
316
335
|
const mcpContext = createToolContext({
|
|
317
336
|
store,
|
|
@@ -355,6 +374,7 @@ export async function startResidentRuntime(
|
|
|
355
374
|
toolMutex,
|
|
356
375
|
readerGate,
|
|
357
376
|
jobManager,
|
|
377
|
+
capsuleReverificationScheduler,
|
|
358
378
|
modelManager,
|
|
359
379
|
mcpContext,
|
|
360
380
|
generations,
|
|
@@ -453,6 +473,7 @@ export async function startResidentRuntime(
|
|
|
453
473
|
syncOptions.triggerEmbed === false
|
|
454
474
|
? null
|
|
455
475
|
: await scheduler.triggerNow();
|
|
476
|
+
capsuleReverificationScheduler.notifySyncSettled();
|
|
456
477
|
return { syncResult, embedResult };
|
|
457
478
|
},
|
|
458
479
|
async dispose() {
|
|
@@ -466,6 +487,7 @@ export async function startResidentRuntime(
|
|
|
466
487
|
);
|
|
467
488
|
if (deadlineReached) shutdownState = "deadline";
|
|
468
489
|
await backgroundWork.cancelAndDrain();
|
|
490
|
+
await capsuleReverificationScheduler.dispose();
|
|
469
491
|
await jobManager.shutdown().catch(() => undefined);
|
|
470
492
|
await Promise.allSettled([
|
|
471
493
|
Promise.resolve().then(() => watchService.dispose()),
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -160,6 +160,7 @@ import {
|
|
|
160
160
|
withRetrievalTraceHeader,
|
|
161
161
|
} from "../retrieval-trace";
|
|
162
162
|
import { buildAppStatus, type StatusBuildDeps } from "../status";
|
|
163
|
+
import { handleChanges, handleDiff, handleImpact } from "./changes";
|
|
163
164
|
|
|
164
165
|
/** Mutable context holder for hot-reloading presets */
|
|
165
166
|
export interface ContextHolder {
|
|
@@ -5122,6 +5123,18 @@ export async function routeApi(
|
|
|
5122
5123
|
);
|
|
5123
5124
|
}
|
|
5124
5125
|
|
|
5126
|
+
if (path === "/api/changes" && req.method === "GET") {
|
|
5127
|
+
return handleChanges(store, url);
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
if (path === "/api/diff" && req.method === "GET") {
|
|
5131
|
+
return handleDiff(store, url);
|
|
5132
|
+
}
|
|
5133
|
+
|
|
5134
|
+
if (path === "/api/impact" && req.method === "GET") {
|
|
5135
|
+
return handleImpact(store, url);
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5125
5138
|
// Unknown API route
|
|
5126
5139
|
if (path.startsWith("/api/")) {
|
|
5127
5140
|
return errorResponse("NOT_FOUND", `Unknown API endpoint: ${path}`, 404);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Read-only REST handlers for knowledge change, diff, and impact services. */
|
|
2
|
+
|
|
3
|
+
import type { KnowledgeImpactInput } from "../../core/knowledge-delta";
|
|
4
|
+
import type { StorePort } from "../../store/types";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
analyzeKnowledgeImpact,
|
|
8
|
+
getKnowledgeDiff,
|
|
9
|
+
listKnowledgeChanges,
|
|
10
|
+
} from "../../core/knowledge-delta";
|
|
11
|
+
|
|
12
|
+
const errorResponse = (
|
|
13
|
+
code: "VALIDATION" | "RUNTIME",
|
|
14
|
+
message: string
|
|
15
|
+
): Response =>
|
|
16
|
+
Response.json(
|
|
17
|
+
{ error: { code, message } },
|
|
18
|
+
{ status: code === "VALIDATION" ? 400 : 500 }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const parsePositiveInt = (
|
|
22
|
+
url: URL,
|
|
23
|
+
name: string
|
|
24
|
+
): number | undefined | Response => {
|
|
25
|
+
const raw = url.searchParams.get(name);
|
|
26
|
+
if (raw === null) return;
|
|
27
|
+
if (!/^\d+$/.test(raw)) {
|
|
28
|
+
return errorResponse("VALIDATION", `${name} must be a positive integer`);
|
|
29
|
+
}
|
|
30
|
+
const value = Number(raw);
|
|
31
|
+
return Number.isSafeInteger(value) && value > 0
|
|
32
|
+
? value
|
|
33
|
+
: errorResponse("VALIDATION", `${name} must be a positive integer`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function handleChanges(
|
|
37
|
+
store: StorePort,
|
|
38
|
+
url: URL
|
|
39
|
+
): Promise<Response> {
|
|
40
|
+
const limit = parsePositiveInt(url, "limit");
|
|
41
|
+
if (limit instanceof Response) return limit;
|
|
42
|
+
const result = await listKnowledgeChanges(store, {
|
|
43
|
+
since: url.searchParams.get("since") ?? undefined,
|
|
44
|
+
collection: url.searchParams.get("collection") ?? undefined,
|
|
45
|
+
limit,
|
|
46
|
+
});
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
return errorResponse(
|
|
49
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
50
|
+
result.error
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return Response.json(result.data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function handleDiff(
|
|
57
|
+
store: StorePort,
|
|
58
|
+
url: URL
|
|
59
|
+
): Promise<Response> {
|
|
60
|
+
const ref = url.searchParams.get("ref")?.trim();
|
|
61
|
+
if (!ref) return errorResponse("VALIDATION", "ref is required");
|
|
62
|
+
const result = await getKnowledgeDiff(
|
|
63
|
+
store,
|
|
64
|
+
ref,
|
|
65
|
+
url.searchParams.get("change") ?? undefined
|
|
66
|
+
);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
return errorResponse(
|
|
69
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
70
|
+
result.error
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return Response.json(result.data);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function handleImpact(
|
|
77
|
+
store: StorePort,
|
|
78
|
+
url: URL
|
|
79
|
+
): Promise<Response> {
|
|
80
|
+
const ref = url.searchParams.get("ref")?.trim();
|
|
81
|
+
if (!ref) return errorResponse("VALIDATION", "ref is required");
|
|
82
|
+
const input: KnowledgeImpactInput = {};
|
|
83
|
+
for (const [queryName, inputName] of [
|
|
84
|
+
["maxDepth", "maxDepth"],
|
|
85
|
+
["maxNodes", "maxNodes"],
|
|
86
|
+
["maxEdges", "maxEdges"],
|
|
87
|
+
["frontierLimit", "frontierLimit"],
|
|
88
|
+
["visitedLimit", "visitedLimit"],
|
|
89
|
+
] as const) {
|
|
90
|
+
const value = parsePositiveInt(url, queryName);
|
|
91
|
+
if (value instanceof Response) return value;
|
|
92
|
+
if (value !== undefined) input[inputName] = value;
|
|
93
|
+
}
|
|
94
|
+
const result = await analyzeKnowledgeImpact(store, ref, input);
|
|
95
|
+
if (!result.success) {
|
|
96
|
+
return errorResponse(
|
|
97
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
98
|
+
result.error
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return Response.json(result.data);
|
|
102
|
+
}
|
package/src/serve/server.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
handleUpdateDoc,
|
|
70
70
|
handleVerifyConnector,
|
|
71
71
|
} from "./routes/api";
|
|
72
|
+
import { handleChanges, handleDiff, handleImpact } from "./routes/changes";
|
|
72
73
|
import { handleGraph, handleGraphQuery } from "./routes/graph";
|
|
73
74
|
import {
|
|
74
75
|
handleDocBacklinks,
|
|
@@ -968,6 +969,39 @@ export async function startServer(
|
|
|
968
969
|
);
|
|
969
970
|
},
|
|
970
971
|
},
|
|
972
|
+
"/api/changes": {
|
|
973
|
+
GET: async (req: Request) => {
|
|
974
|
+
const url = new URL(req.url);
|
|
975
|
+
return withSecurityHeaders(
|
|
976
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
977
|
+
handleChanges(store, url)
|
|
978
|
+
),
|
|
979
|
+
isDev
|
|
980
|
+
);
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
"/api/diff": {
|
|
984
|
+
GET: async (req: Request) => {
|
|
985
|
+
const url = new URL(req.url);
|
|
986
|
+
return withSecurityHeaders(
|
|
987
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
988
|
+
handleDiff(store, url)
|
|
989
|
+
),
|
|
990
|
+
isDev
|
|
991
|
+
);
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
"/api/impact": {
|
|
995
|
+
GET: async (req: Request) => {
|
|
996
|
+
const url = new URL(req.url);
|
|
997
|
+
return withSecurityHeaders(
|
|
998
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
999
|
+
handleImpact(store, url)
|
|
1000
|
+
),
|
|
1001
|
+
isDev
|
|
1002
|
+
);
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
971
1005
|
},
|
|
972
1006
|
});
|
|
973
1007
|
} catch (e) {
|
|
@@ -31,6 +31,8 @@ export interface CollectionWatchCallbacks {
|
|
|
31
31
|
relPaths: string[];
|
|
32
32
|
error: unknown;
|
|
33
33
|
}) => void;
|
|
34
|
+
/** Fires after all watcher syncs and queued paths have settled. */
|
|
35
|
+
onSettled?: () => void;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
interface CollectionWatchServiceOptions {
|
|
@@ -236,6 +238,13 @@ export class CollectionWatchService {
|
|
|
236
238
|
const remaining = this.#pendingByCollection.get(collectionName);
|
|
237
239
|
if (remaining && remaining.size > 0) {
|
|
238
240
|
void this.#flushCollection(collectionName);
|
|
241
|
+
} else if (
|
|
242
|
+
this.#syncing.size === 0 &&
|
|
243
|
+
![...this.#pendingByCollection.values()].some(
|
|
244
|
+
(relPaths) => relPaths.size > 0
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
this.#callbacks?.onSettled?.();
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
}
|
package/src/store/index.ts
CHANGED
|
@@ -33,6 +33,16 @@ export type {
|
|
|
33
33
|
CollectionRow,
|
|
34
34
|
CollectionStatus,
|
|
35
35
|
ContextRow,
|
|
36
|
+
DocumentChangeDateDelta,
|
|
37
|
+
DocumentChangeKind,
|
|
38
|
+
DocumentChangeListOptions,
|
|
39
|
+
DocumentChangePage,
|
|
40
|
+
DocumentChangePurgeResult,
|
|
41
|
+
DocumentChangeRetentionPolicy,
|
|
42
|
+
DocumentChangeRetentionResult,
|
|
43
|
+
DocumentChangeRow,
|
|
44
|
+
DocumentChangeSet,
|
|
45
|
+
DocumentChangeStructureDelta,
|
|
36
46
|
DocumentInput,
|
|
37
47
|
DocumentRow,
|
|
38
48
|
FtsResult,
|
|
@@ -72,6 +82,17 @@ export type {
|
|
|
72
82
|
RetrievalTraceRunRow,
|
|
73
83
|
RetrievalTraceStatus,
|
|
74
84
|
RetrievalTraceTerminalStatus,
|
|
85
|
+
RenameDocumentOptions,
|
|
86
|
+
SavedCapsuleAffectedQuestionState,
|
|
87
|
+
SavedCapsuleEvidenceReference,
|
|
88
|
+
SavedCapsuleNotificationPreference,
|
|
89
|
+
SavedCapsuleOperationStatus,
|
|
90
|
+
SavedCapsuleRegistration,
|
|
91
|
+
SavedCapsuleRegistrationInput,
|
|
92
|
+
SavedCapsuleRegistrationRecord,
|
|
93
|
+
SavedCapsuleReverificationState,
|
|
94
|
+
SavedCapsuleTriggerKind,
|
|
95
|
+
SavedCapsuleVerificationRecord,
|
|
75
96
|
StoreError,
|
|
76
97
|
StoreErrorCode,
|
|
77
98
|
StorePort,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: bounded, metadata-only document change journal.
|
|
3
|
+
*
|
|
4
|
+
* The journal intentionally stores document identity, hashes, active state,
|
|
5
|
+
* and compact structural summaries. Source and converted document bodies are
|
|
6
|
+
* never copied into this table.
|
|
7
|
+
*
|
|
8
|
+
* @module src/store/migrations/015-document-change-journal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Database } from "bun:sqlite";
|
|
12
|
+
|
|
13
|
+
import type { Migration } from "./runner";
|
|
14
|
+
|
|
15
|
+
export const migration: Migration = {
|
|
16
|
+
version: 15,
|
|
17
|
+
name: "document_change_journal",
|
|
18
|
+
|
|
19
|
+
up(db: Database): void {
|
|
20
|
+
db.exec(`
|
|
21
|
+
CREATE TABLE document_changes (
|
|
22
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
document_id INTEGER NOT NULL CHECK (document_id > 0),
|
|
24
|
+
collection TEXT NOT NULL,
|
|
25
|
+
change_kind TEXT NOT NULL
|
|
26
|
+
CHECK (change_kind IN ('create', 'update', 'rename', 'inactivate', 'reactivate')),
|
|
27
|
+
old_rel_path TEXT,
|
|
28
|
+
new_rel_path TEXT,
|
|
29
|
+
old_docid TEXT,
|
|
30
|
+
new_docid TEXT,
|
|
31
|
+
old_uri TEXT,
|
|
32
|
+
new_uri TEXT,
|
|
33
|
+
old_source_hash TEXT,
|
|
34
|
+
new_source_hash TEXT,
|
|
35
|
+
old_mirror_hash TEXT,
|
|
36
|
+
new_mirror_hash TEXT,
|
|
37
|
+
old_active INTEGER CHECK (old_active IS NULL OR old_active IN (0, 1)),
|
|
38
|
+
new_active INTEGER CHECK (new_active IS NULL OR new_active IN (0, 1)),
|
|
39
|
+
heading_delta_json TEXT NOT NULL DEFAULT '{"added":[],"removed":[]}',
|
|
40
|
+
link_delta_json TEXT NOT NULL DEFAULT '{"added":[],"removed":[]}',
|
|
41
|
+
typed_edge_delta_json TEXT NOT NULL DEFAULT '{"added":[],"removed":[]}',
|
|
42
|
+
date_delta_json TEXT NOT NULL DEFAULT '{"added":[],"removed":[],"changed":[]}',
|
|
43
|
+
structure_truncated INTEGER NOT NULL DEFAULT 0
|
|
44
|
+
CHECK (structure_truncated IN (0, 1)),
|
|
45
|
+
observed_at_ms INTEGER NOT NULL CHECK (observed_at_ms >= 0),
|
|
46
|
+
byte_size INTEGER NOT NULL CHECK (byte_size > 0 AND byte_size <= 131072),
|
|
47
|
+
CHECK (length(CAST(collection AS BLOB)) BETWEEN 1 AND 256),
|
|
48
|
+
CHECK (old_rel_path IS NULL OR length(CAST(old_rel_path AS BLOB)) BETWEEN 1 AND 4096),
|
|
49
|
+
CHECK (new_rel_path IS NULL OR length(CAST(new_rel_path AS BLOB)) BETWEEN 1 AND 4096),
|
|
50
|
+
CHECK (old_docid IS NULL OR length(CAST(old_docid AS BLOB)) BETWEEN 1 AND 256),
|
|
51
|
+
CHECK (new_docid IS NULL OR length(CAST(new_docid AS BLOB)) BETWEEN 1 AND 256),
|
|
52
|
+
CHECK (old_uri IS NULL OR length(CAST(old_uri AS BLOB)) BETWEEN 1 AND 8192),
|
|
53
|
+
CHECK (new_uri IS NULL OR length(CAST(new_uri AS BLOB)) BETWEEN 1 AND 8192),
|
|
54
|
+
CHECK (old_source_hash IS NULL OR length(CAST(old_source_hash AS BLOB)) BETWEEN 1 AND 256),
|
|
55
|
+
CHECK (new_source_hash IS NULL OR length(CAST(new_source_hash AS BLOB)) BETWEEN 1 AND 256),
|
|
56
|
+
CHECK (old_mirror_hash IS NULL OR length(CAST(old_mirror_hash AS BLOB)) BETWEEN 1 AND 256),
|
|
57
|
+
CHECK (new_mirror_hash IS NULL OR length(CAST(new_mirror_hash AS BLOB)) BETWEEN 1 AND 256),
|
|
58
|
+
CHECK (length(CAST(heading_delta_json AS BLOB)) <= 16384),
|
|
59
|
+
CHECK (length(CAST(link_delta_json AS BLOB)) <= 16384),
|
|
60
|
+
CHECK (length(CAST(typed_edge_delta_json AS BLOB)) <= 16384),
|
|
61
|
+
CHECK (length(CAST(date_delta_json AS BLOB)) <= 16384)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX idx_document_changes_collection_sequence
|
|
65
|
+
ON document_changes(collection, sequence);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX idx_document_changes_document_sequence
|
|
68
|
+
ON document_changes(document_id, sequence);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX idx_document_changes_retention
|
|
71
|
+
ON document_changes(observed_at_ms, sequence);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE document_change_journal_state (
|
|
74
|
+
singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
|
|
75
|
+
last_sequence INTEGER NOT NULL DEFAULT 0 CHECK (last_sequence >= 0),
|
|
76
|
+
retention_floor INTEGER NOT NULL DEFAULT 0
|
|
77
|
+
CHECK (retention_floor >= 0 AND retention_floor <= last_sequence)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
INSERT INTO document_change_journal_state (
|
|
81
|
+
singleton_id, last_sequence, retention_floor
|
|
82
|
+
) VALUES (1, 0, 0);
|
|
83
|
+
`);
|
|
84
|
+
},
|
|
85
|
+
};
|