@gmickel/gno 1.19.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 +28 -8
- package/assets/skill/SKILL.md +73 -27
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +1 -1
- package/spec/cli.md +142 -17
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +87 -5
- package/spec/mcp.md +75 -3
- package/spec/output-schemas/ask.schema.json +198 -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/claim-verification.schema.json +291 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
- 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/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +4 -0
- package/src/app/context-runtime.ts +5 -1
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +40 -149
- 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 +227 -1
- 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/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +33 -21
- package/src/core/context-evidence.ts +6 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/core/retrieval-trace-evidence-origin.ts +3 -0
- package/src/core/retrieval-trace-session.ts +15 -2
- package/src/ingestion/sync.ts +214 -165
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/context.ts +28 -7
- package/src/mcp/tools/index.ts +38 -0
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/types.ts +25 -0
- package/src/sdk/client.ts +77 -2
- 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/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +162 -3
- 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
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
SourcesTrigger,
|
|
22
22
|
} from "../components/ai-elements/sources";
|
|
23
23
|
import { AIModelSelector } from "../components/AIModelSelector";
|
|
24
|
+
import {
|
|
25
|
+
type AskVerification,
|
|
26
|
+
AskVerificationPanel,
|
|
27
|
+
} from "../components/AskVerificationPanel";
|
|
24
28
|
import {
|
|
25
29
|
ThoroughnessSelector,
|
|
26
30
|
type Thoroughness,
|
|
@@ -57,6 +61,7 @@ interface PageProps {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
interface Citation {
|
|
64
|
+
evidenceId?: string;
|
|
60
65
|
docid: string;
|
|
61
66
|
uri: string;
|
|
62
67
|
startLine?: number;
|
|
@@ -88,12 +93,15 @@ interface AskResponse {
|
|
|
88
93
|
vectorsUsed: boolean;
|
|
89
94
|
answerGenerated: boolean;
|
|
90
95
|
totalResults: number;
|
|
96
|
+
verificationRequested?: boolean;
|
|
97
|
+
abstained?: boolean;
|
|
91
98
|
queryModes?: {
|
|
92
99
|
term: number;
|
|
93
100
|
intent: number;
|
|
94
101
|
hyde: boolean;
|
|
95
102
|
};
|
|
96
103
|
};
|
|
104
|
+
verification?: AskVerification;
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
interface Capabilities {
|
|
@@ -139,7 +147,7 @@ function renderAnswer(
|
|
|
139
147
|
const parts: React.ReactNode[] = [];
|
|
140
148
|
let key = 0;
|
|
141
149
|
|
|
142
|
-
const citationRegex = /\[(\d+)\]/g;
|
|
150
|
+
const citationRegex = /\[(?:evidence:([a-f0-9]{64})|(\d+))\]/g;
|
|
143
151
|
let lastIndex = 0;
|
|
144
152
|
let match: RegExpExecArray | null;
|
|
145
153
|
|
|
@@ -149,8 +157,13 @@ function renderAnswer(
|
|
|
149
157
|
parts.push(answer.slice(lastIndex, match.index));
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
const
|
|
153
|
-
const
|
|
160
|
+
const evidenceId = match[1];
|
|
161
|
+
const numericCitation = match[2] ? Number(match[2]) : undefined;
|
|
162
|
+
const citationIndex = evidenceId
|
|
163
|
+
? citations.findIndex((citation) => citation.evidenceId === evidenceId)
|
|
164
|
+
: (numericCitation ?? 0) - 1;
|
|
165
|
+
const citationNum = citationIndex + 1;
|
|
166
|
+
const citation = citations[citationIndex];
|
|
154
167
|
|
|
155
168
|
if (citation) {
|
|
156
169
|
parts.push(
|
|
@@ -186,6 +199,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
186
199
|
const [collections, setCollections] = useState<Collection[]>([]);
|
|
187
200
|
const [thoroughness, setThoroughness] = useState<Thoroughness>("balanced");
|
|
188
201
|
const [activePreset, setActivePreset] = useState("slim-tuned");
|
|
202
|
+
const [verify, setVerify] = useState(false);
|
|
189
203
|
|
|
190
204
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
191
205
|
const [selectedCollection, setSelectedCollection] = useState("");
|
|
@@ -318,6 +332,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
318
332
|
const requestBody: Record<string, unknown> = {
|
|
319
333
|
query: currentQuery,
|
|
320
334
|
limit: 5,
|
|
335
|
+
verify,
|
|
321
336
|
};
|
|
322
337
|
|
|
323
338
|
if (selectedCollection) {
|
|
@@ -394,6 +409,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
394
409
|
tagsInput,
|
|
395
410
|
thoroughness,
|
|
396
411
|
until,
|
|
412
|
+
verify,
|
|
397
413
|
]
|
|
398
414
|
);
|
|
399
415
|
|
|
@@ -443,6 +459,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
443
459
|
parseTagsCsv(tagsInput).length > 0
|
|
444
460
|
? `${tagMode}:${parseTagsCsv(tagsInput).join(",")}`
|
|
445
461
|
: null,
|
|
462
|
+
verify ? "verified" : null,
|
|
446
463
|
].filter((pill): pill is string => Boolean(pill));
|
|
447
464
|
|
|
448
465
|
return (
|
|
@@ -475,6 +492,16 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
475
492
|
value={thoroughness}
|
|
476
493
|
/>
|
|
477
494
|
|
|
495
|
+
<Button
|
|
496
|
+
aria-pressed={verify}
|
|
497
|
+
onClick={() => setVerify((value) => !value)}
|
|
498
|
+
size="sm"
|
|
499
|
+
type="button"
|
|
500
|
+
variant={verify ? "default" : "outline"}
|
|
501
|
+
>
|
|
502
|
+
Verify
|
|
503
|
+
</Button>
|
|
504
|
+
|
|
478
505
|
<div className="h-6 w-px bg-border/40" />
|
|
479
506
|
|
|
480
507
|
<AIModelSelector onPresetChange={setActivePreset} />
|
|
@@ -878,8 +905,19 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
878
905
|
</Sources>
|
|
879
906
|
)}
|
|
880
907
|
|
|
908
|
+
{entry.response.verification && (
|
|
909
|
+
<AskVerificationPanel
|
|
910
|
+
navigate={navigate}
|
|
911
|
+
verification={entry.response.verification}
|
|
912
|
+
/>
|
|
913
|
+
)}
|
|
914
|
+
|
|
881
915
|
<div className="flex items-center gap-2 text-muted-foreground/60 text-xs">
|
|
882
|
-
<span>
|
|
916
|
+
<span>
|
|
917
|
+
{entry.response.verification
|
|
918
|
+
? `${entry.response.verification.capsule.evidence.length} evidence spans`
|
|
919
|
+
: `${entry.response.results.length} results`}
|
|
920
|
+
</span>
|
|
883
921
|
{entry.response.meta.vectorsUsed && (
|
|
884
922
|
<Badge
|
|
885
923
|
className="font-mono text-[9px]"
|
|
@@ -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
|
@@ -36,6 +36,7 @@ import type { StartJobError } from "../jobs";
|
|
|
36
36
|
import type { ResidentStatus } from "../status-model";
|
|
37
37
|
import type { CollectionWatchService } from "../watch-service";
|
|
38
38
|
|
|
39
|
+
import { buildVerifiedAsk } from "../../app/verified-ask";
|
|
39
40
|
import { modelsPull } from "../../cli/commands/models/pull";
|
|
40
41
|
import {
|
|
41
42
|
addCollection,
|
|
@@ -125,6 +126,7 @@ import {
|
|
|
125
126
|
resolveModelUri,
|
|
126
127
|
} from "../../llm/registry";
|
|
127
128
|
import {
|
|
129
|
+
answerTraceTerminalStatus,
|
|
128
130
|
generateGroundedAnswer,
|
|
129
131
|
processAnswerResultWithTrace,
|
|
130
132
|
} from "../../pipeline/answer";
|
|
@@ -158,6 +160,7 @@ import {
|
|
|
158
160
|
withRetrievalTraceHeader,
|
|
159
161
|
} from "../retrieval-trace";
|
|
160
162
|
import { buildAppStatus, type StatusBuildDeps } from "../status";
|
|
163
|
+
import { handleChanges, handleDiff, handleImpact } from "./changes";
|
|
161
164
|
|
|
162
165
|
/** Mutable context holder for hot-reloading presets */
|
|
163
166
|
export interface ContextHolder {
|
|
@@ -289,14 +292,46 @@ export interface AskRequestBody {
|
|
|
289
292
|
category?: string;
|
|
290
293
|
author?: string;
|
|
291
294
|
maxAnswerTokens?: number;
|
|
295
|
+
verify?: boolean;
|
|
296
|
+
contextBudgetTokens?: number;
|
|
297
|
+
contextBudgetBytes?: number;
|
|
298
|
+
minScore?: number;
|
|
292
299
|
noExpand?: boolean;
|
|
293
300
|
noRerank?: boolean;
|
|
301
|
+
graph?: boolean;
|
|
302
|
+
noGraph?: boolean;
|
|
294
303
|
/** Comma-separated tags - filter to docs having ALL (AND) */
|
|
295
304
|
tagsAll?: string;
|
|
296
305
|
/** Comma-separated tags - filter to docs having ANY (OR) */
|
|
297
306
|
tagsAny?: string;
|
|
298
307
|
}
|
|
299
308
|
|
|
309
|
+
const ASK_REQUEST_KEYS = new Set<keyof AskRequestBody>([
|
|
310
|
+
"query",
|
|
311
|
+
"limit",
|
|
312
|
+
"collection",
|
|
313
|
+
"lang",
|
|
314
|
+
"intent",
|
|
315
|
+
"candidateLimit",
|
|
316
|
+
"exclude",
|
|
317
|
+
"queryModes",
|
|
318
|
+
"since",
|
|
319
|
+
"until",
|
|
320
|
+
"category",
|
|
321
|
+
"author",
|
|
322
|
+
"maxAnswerTokens",
|
|
323
|
+
"verify",
|
|
324
|
+
"contextBudgetTokens",
|
|
325
|
+
"contextBudgetBytes",
|
|
326
|
+
"minScore",
|
|
327
|
+
"noExpand",
|
|
328
|
+
"noRerank",
|
|
329
|
+
"graph",
|
|
330
|
+
"noGraph",
|
|
331
|
+
"tagsAll",
|
|
332
|
+
"tagsAny",
|
|
333
|
+
]);
|
|
334
|
+
|
|
300
335
|
export interface CreateCollectionRequestBody {
|
|
301
336
|
path: string;
|
|
302
337
|
name?: string;
|
|
@@ -512,11 +547,12 @@ async function startRestTrace(
|
|
|
512
547
|
async function finishRestTrace(
|
|
513
548
|
request: Request,
|
|
514
549
|
session: RetrievalTraceSession | null,
|
|
515
|
-
status: "completed" | "partial" | "failed",
|
|
550
|
+
status: "completed" | "partial" | "failed" | "cancelled",
|
|
516
551
|
response: Response
|
|
517
552
|
): Promise<Response> {
|
|
518
553
|
if (!session) return response;
|
|
519
|
-
const terminalStatus =
|
|
554
|
+
const terminalStatus =
|
|
555
|
+
request.signal.aborted || status === "cancelled" ? "cancelled" : status;
|
|
520
556
|
const finished = await session.finish(terminalStatus);
|
|
521
557
|
if (!finished.ok) {
|
|
522
558
|
return withRetrievalTraceHeader(
|
|
@@ -3503,6 +3539,9 @@ export async function handleSearch(
|
|
|
3503
3539
|
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
3504
3540
|
}
|
|
3505
3541
|
|
|
3542
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
3543
|
+
return errorResponse("VALIDATION", "Request body must be an object");
|
|
3544
|
+
}
|
|
3506
3545
|
if (!body.query || typeof body.query !== "string") {
|
|
3507
3546
|
return errorResponse("VALIDATION", "Missing or invalid query");
|
|
3508
3547
|
}
|
|
@@ -4028,15 +4067,84 @@ export async function handleAsk(
|
|
|
4028
4067
|
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
4029
4068
|
}
|
|
4030
4069
|
|
|
4070
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
4071
|
+
return errorResponse("VALIDATION", "Request body must be an object");
|
|
4072
|
+
}
|
|
4073
|
+
const unknownKey = Object.keys(body).find(
|
|
4074
|
+
(key) => !ASK_REQUEST_KEYS.has(key as keyof AskRequestBody)
|
|
4075
|
+
);
|
|
4076
|
+
if (unknownKey) {
|
|
4077
|
+
return errorResponse(
|
|
4078
|
+
"VALIDATION",
|
|
4079
|
+
`Unknown Ask request field: ${unknownKey}`
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
4031
4082
|
if (!body.query || typeof body.query !== "string") {
|
|
4032
4083
|
return errorResponse("VALIDATION", "Missing or invalid query");
|
|
4033
4084
|
}
|
|
4034
4085
|
|
|
4086
|
+
for (const field of [
|
|
4087
|
+
"verify",
|
|
4088
|
+
"noExpand",
|
|
4089
|
+
"noRerank",
|
|
4090
|
+
"graph",
|
|
4091
|
+
"noGraph",
|
|
4092
|
+
] as const) {
|
|
4093
|
+
if (body[field] !== undefined && typeof body[field] !== "boolean") {
|
|
4094
|
+
return errorResponse("VALIDATION", `${field} must be a boolean`);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
for (const [field, value, maximum] of [
|
|
4098
|
+
["limit", body.limit, 20],
|
|
4099
|
+
["candidateLimit", body.candidateLimit, 100],
|
|
4100
|
+
["maxAnswerTokens", body.maxAnswerTokens, Number.MAX_SAFE_INTEGER],
|
|
4101
|
+
] as const) {
|
|
4102
|
+
if (
|
|
4103
|
+
value !== undefined &&
|
|
4104
|
+
(!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
4105
|
+
) {
|
|
4106
|
+
return errorResponse(
|
|
4107
|
+
"VALIDATION",
|
|
4108
|
+
`${field} must be a positive integer${maximum < Number.MAX_SAFE_INTEGER ? ` no greater than ${maximum}` : ""}`
|
|
4109
|
+
);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4035
4113
|
const rawQuery = body.query.trim();
|
|
4036
4114
|
if (!rawQuery) {
|
|
4037
4115
|
return errorResponse("VALIDATION", "Query cannot be empty");
|
|
4038
4116
|
}
|
|
4039
4117
|
|
|
4118
|
+
if (
|
|
4119
|
+
body.minScore !== undefined &&
|
|
4120
|
+
(typeof body.minScore !== "number" ||
|
|
4121
|
+
!Number.isFinite(body.minScore) ||
|
|
4122
|
+
body.minScore < 0 ||
|
|
4123
|
+
body.minScore > 1)
|
|
4124
|
+
) {
|
|
4125
|
+
return errorResponse("VALIDATION", "minScore must be between 0 and 1");
|
|
4126
|
+
}
|
|
4127
|
+
if (
|
|
4128
|
+
body.contextBudgetTokens !== undefined &&
|
|
4129
|
+
(!Number.isSafeInteger(body.contextBudgetTokens) ||
|
|
4130
|
+
body.contextBudgetTokens < 1)
|
|
4131
|
+
) {
|
|
4132
|
+
return errorResponse(
|
|
4133
|
+
"VALIDATION",
|
|
4134
|
+
"contextBudgetTokens must be a positive integer"
|
|
4135
|
+
);
|
|
4136
|
+
}
|
|
4137
|
+
if (
|
|
4138
|
+
body.contextBudgetBytes !== undefined &&
|
|
4139
|
+
(!Number.isSafeInteger(body.contextBudgetBytes) ||
|
|
4140
|
+
body.contextBudgetBytes < 1)
|
|
4141
|
+
) {
|
|
4142
|
+
return errorResponse(
|
|
4143
|
+
"VALIDATION",
|
|
4144
|
+
"contextBudgetBytes must be a positive integer"
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4040
4148
|
// Parse tag filters
|
|
4041
4149
|
let tagsAll: string[] | undefined;
|
|
4042
4150
|
let tagsAny: string[] | undefined;
|
|
@@ -4128,6 +4236,7 @@ export async function handleAsk(
|
|
|
4128
4236
|
collection: body.collection,
|
|
4129
4237
|
lang: body.lang,
|
|
4130
4238
|
intent: body.intent?.trim() || undefined,
|
|
4239
|
+
minScore: body.minScore,
|
|
4131
4240
|
noExpand: body.noExpand,
|
|
4132
4241
|
noRerank: body.noRerank,
|
|
4133
4242
|
candidateLimit:
|
|
@@ -4135,6 +4244,8 @@ export async function handleAsk(
|
|
|
4135
4244
|
? Math.min(body.candidateLimit, 100)
|
|
4136
4245
|
: undefined,
|
|
4137
4246
|
exclude,
|
|
4247
|
+
graph: body.graph,
|
|
4248
|
+
noGraph: body.noGraph,
|
|
4138
4249
|
queryModes: normalizedQueryModes,
|
|
4139
4250
|
tagsAll,
|
|
4140
4251
|
tagsAny,
|
|
@@ -4142,6 +4253,10 @@ export async function handleAsk(
|
|
|
4142
4253
|
until: body.until,
|
|
4143
4254
|
categories,
|
|
4144
4255
|
author,
|
|
4256
|
+
verify: body.verify,
|
|
4257
|
+
contextBudgetTokens: body.contextBudgetTokens,
|
|
4258
|
+
contextBudgetBytes: body.contextBudgetBytes,
|
|
4259
|
+
maxAnswerTokens: body.maxAnswerTokens,
|
|
4145
4260
|
};
|
|
4146
4261
|
const trace = await startRestTrace(ctx, {
|
|
4147
4262
|
query: normalizedQuery,
|
|
@@ -4166,7 +4281,7 @@ export async function handleAsk(
|
|
|
4166
4281
|
return finishRestTrace(
|
|
4167
4282
|
req,
|
|
4168
4283
|
trace.session,
|
|
4169
|
-
"failed",
|
|
4284
|
+
req.signal.aborted ? "cancelled" : "failed",
|
|
4170
4285
|
errorResponse("RUNTIME", unavailable.error.message, 500)
|
|
4171
4286
|
);
|
|
4172
4287
|
}
|
|
@@ -4182,6 +4297,38 @@ export async function handleAsk(
|
|
|
4182
4297
|
);
|
|
4183
4298
|
}
|
|
4184
4299
|
|
|
4300
|
+
if (body.verify && ctx.answerPort) {
|
|
4301
|
+
try {
|
|
4302
|
+
const verified = await buildVerifiedAsk(normalizedQuery, askOptions, {
|
|
4303
|
+
store: ctx.store,
|
|
4304
|
+
config: ctx.config,
|
|
4305
|
+
indexName: ctx.indexName,
|
|
4306
|
+
vectorIndex: ctx.vectorIndex,
|
|
4307
|
+
embedPort: ctx.embedPort,
|
|
4308
|
+
rerankPort: ctx.rerankPort,
|
|
4309
|
+
genPort: ctx.answerPort,
|
|
4310
|
+
traceSession: trace.session ?? undefined,
|
|
4311
|
+
});
|
|
4312
|
+
return finishRestTrace(
|
|
4313
|
+
req,
|
|
4314
|
+
trace.session,
|
|
4315
|
+
answerTraceTerminalStatus(verified.citations),
|
|
4316
|
+
jsonResponse(verified)
|
|
4317
|
+
);
|
|
4318
|
+
} catch (error) {
|
|
4319
|
+
return finishRestTrace(
|
|
4320
|
+
req,
|
|
4321
|
+
trace.session,
|
|
4322
|
+
"failed",
|
|
4323
|
+
errorResponse(
|
|
4324
|
+
"RUNTIME",
|
|
4325
|
+
error instanceof Error ? error.message : String(error),
|
|
4326
|
+
500
|
|
4327
|
+
)
|
|
4328
|
+
);
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4185
4332
|
// Run hybrid search first
|
|
4186
4333
|
const searchResult = await searchHybrid(
|
|
4187
4334
|
{
|
|
@@ -4976,6 +5123,18 @@ export async function routeApi(
|
|
|
4976
5123
|
);
|
|
4977
5124
|
}
|
|
4978
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
|
+
|
|
4979
5138
|
// Unknown API route
|
|
4980
5139
|
if (path.startsWith("/api/")) {
|
|
4981
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,
|