@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
package/src/cli/commands/ask.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from "../../llm/types";
|
|
15
15
|
import type { AskOptions, AskResult, Citation } from "../../pipeline/types";
|
|
16
16
|
|
|
17
|
+
import { buildVerifiedAsk } from "../../app/verified-ask";
|
|
17
18
|
import {
|
|
18
19
|
finishRetrievalTraceAfterError,
|
|
19
20
|
retrievalTraceFilters,
|
|
@@ -39,6 +40,8 @@ import {
|
|
|
39
40
|
} from "../progress";
|
|
40
41
|
import { initStore } from "./shared";
|
|
41
42
|
|
|
43
|
+
export { formatAsk } from "./ask-format";
|
|
44
|
+
|
|
42
45
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
46
|
// Types
|
|
44
47
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -83,9 +86,11 @@ export async function ask(
|
|
|
83
86
|
options: AskCommandOptions = {}
|
|
84
87
|
): Promise<AskCommandResult> {
|
|
85
88
|
const limit = options.limit ?? 5;
|
|
89
|
+
const globals = getGlobals();
|
|
86
90
|
|
|
87
91
|
const initResult = await initStore({
|
|
88
92
|
configPath: options.configPath,
|
|
93
|
+
indexName: globals.index,
|
|
89
94
|
collection: options.collection,
|
|
90
95
|
syncConfig: false,
|
|
91
96
|
});
|
|
@@ -103,7 +108,9 @@ export async function ask(
|
|
|
103
108
|
let traceSession: RetrievalTraceSession | undefined;
|
|
104
109
|
|
|
105
110
|
try {
|
|
106
|
-
const
|
|
111
|
+
const verificationRequested = options.verify === true;
|
|
112
|
+
const answerRequested =
|
|
113
|
+
verificationRequested || Boolean(options.answer && !options.noAnswer);
|
|
107
114
|
const embedUri = resolveModelUri(
|
|
108
115
|
config,
|
|
109
116
|
"embed",
|
|
@@ -111,7 +118,7 @@ export async function ask(
|
|
|
111
118
|
options.collection
|
|
112
119
|
);
|
|
113
120
|
const expandUri =
|
|
114
|
-
!options.noExpand && !options.queryModes?.length
|
|
121
|
+
!verificationRequested && !options.noExpand && !options.queryModes?.length
|
|
115
122
|
? resolveModelUri(
|
|
116
123
|
config,
|
|
117
124
|
"expand",
|
|
@@ -136,6 +143,7 @@ export async function ask(
|
|
|
136
143
|
query,
|
|
137
144
|
filters: retrievalTraceFilters({ ...options, limit }),
|
|
138
145
|
pipeline: "ask",
|
|
146
|
+
indexName: globals.index,
|
|
139
147
|
modelUris: [embedUri, expandUri, answerUri, rerankUri].filter(
|
|
140
148
|
(value): value is string => Boolean(value)
|
|
141
149
|
),
|
|
@@ -147,7 +155,6 @@ export async function ask(
|
|
|
147
155
|
const llm = new LlmAdapter(config);
|
|
148
156
|
|
|
149
157
|
// Resolve download policy from env/flags
|
|
150
|
-
const globals = getGlobals();
|
|
151
158
|
const policy = resolveDownloadPolicy(process.env, {
|
|
152
159
|
offline: globals.offline,
|
|
153
160
|
});
|
|
@@ -254,6 +261,34 @@ export async function ask(
|
|
|
254
261
|
};
|
|
255
262
|
}
|
|
256
263
|
|
|
264
|
+
if (verificationRequested && answerPort) {
|
|
265
|
+
const verified = await buildVerifiedAsk(
|
|
266
|
+
query,
|
|
267
|
+
{ ...options, limit },
|
|
268
|
+
{
|
|
269
|
+
store,
|
|
270
|
+
config,
|
|
271
|
+
indexName: globals.index,
|
|
272
|
+
vectorIndex,
|
|
273
|
+
embedPort,
|
|
274
|
+
rerankPort,
|
|
275
|
+
genPort: answerPort,
|
|
276
|
+
traceSession,
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
const finalized = await traceSession?.finish(
|
|
280
|
+
answerTraceTerminalStatus(verified.citations)
|
|
281
|
+
);
|
|
282
|
+
if (finalized && !finalized.ok) {
|
|
283
|
+
return { success: false, error: finalized.error.message };
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
success: true,
|
|
287
|
+
data: verified,
|
|
288
|
+
metadata: traceSession?.metadata(),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
257
292
|
// Run hybrid search
|
|
258
293
|
const searchResult = await searchHybrid(deps, query, {
|
|
259
294
|
limit,
|
|
@@ -267,6 +302,8 @@ export async function ask(
|
|
|
267
302
|
tagsAll: options.tagsAll,
|
|
268
303
|
tagsAny: options.tagsAny,
|
|
269
304
|
exclude: options.exclude,
|
|
305
|
+
minScore: options.minScore,
|
|
306
|
+
graph: options.graph,
|
|
270
307
|
queryModes: options.queryModes,
|
|
271
308
|
noExpand: options.noExpand,
|
|
272
309
|
noRerank: options.noRerank,
|
|
@@ -394,149 +431,3 @@ export async function ask(
|
|
|
394
431
|
await store.close();
|
|
395
432
|
}
|
|
396
433
|
}
|
|
397
|
-
|
|
398
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
399
|
-
// Formatters
|
|
400
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
-
|
|
402
|
-
interface FormatOptions {
|
|
403
|
-
showSources?: boolean;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// oxlint-disable-next-line max-lines-per-function -- terminal formatting with conditional sections
|
|
407
|
-
function formatTerminal(data: AskResult, opts: FormatOptions = {}): string {
|
|
408
|
-
const lines: string[] = [];
|
|
409
|
-
const hasAnswer = Boolean(data.answer);
|
|
410
|
-
|
|
411
|
-
// Show answer if present
|
|
412
|
-
if (data.answer) {
|
|
413
|
-
lines.push("Answer:");
|
|
414
|
-
lines.push(data.answer);
|
|
415
|
-
lines.push("");
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Show cited sources (only sources actually referenced in answer)
|
|
419
|
-
if (data.citations && data.citations.length > 0) {
|
|
420
|
-
lines.push("Cited Sources:");
|
|
421
|
-
for (let i = 0; i < data.citations.length; i++) {
|
|
422
|
-
const c = data.citations[i];
|
|
423
|
-
if (c) {
|
|
424
|
-
lines.push(` [${i + 1}] ${c.uri}`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
lines.push("");
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Show all retrieved sources if:
|
|
431
|
-
// - No answer was generated (retrieval-only mode)
|
|
432
|
-
// - User explicitly requested with --show-sources
|
|
433
|
-
const showAllSources = !hasAnswer || opts.showSources;
|
|
434
|
-
if (showAllSources && data.results.length > 0) {
|
|
435
|
-
lines.push(hasAnswer ? "All Retrieved Sources:" : "Sources:");
|
|
436
|
-
for (const r of data.results) {
|
|
437
|
-
lines.push(` [${r.docid}] ${r.uri}`);
|
|
438
|
-
if (r.title) {
|
|
439
|
-
lines.push(` ${r.title}`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
} else if (hasAnswer && data.results.length > 0) {
|
|
443
|
-
// Hint about --show-sources when we have more sources
|
|
444
|
-
const citedCount = data.citations?.length ?? 0;
|
|
445
|
-
if (data.results.length > citedCount) {
|
|
446
|
-
lines.push(
|
|
447
|
-
`(${data.results.length} sources retrieved, use --show-sources to list all)`
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (!data.answer && data.results.length === 0) {
|
|
453
|
-
lines.push("No relevant sources found.");
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return lines.join("\n");
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function formatMarkdown(data: AskResult, opts: FormatOptions = {}): string {
|
|
460
|
-
const lines: string[] = [];
|
|
461
|
-
const hasAnswer = Boolean(data.answer);
|
|
462
|
-
|
|
463
|
-
lines.push(`# Question: ${data.query}`);
|
|
464
|
-
lines.push("");
|
|
465
|
-
|
|
466
|
-
if (data.answer) {
|
|
467
|
-
lines.push("## Answer");
|
|
468
|
-
lines.push("");
|
|
469
|
-
lines.push(data.answer);
|
|
470
|
-
lines.push("");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Show cited sources (only sources actually referenced in answer)
|
|
474
|
-
if (data.citations && data.citations.length > 0) {
|
|
475
|
-
lines.push("## Cited Sources");
|
|
476
|
-
lines.push("");
|
|
477
|
-
for (let i = 0; i < data.citations.length; i++) {
|
|
478
|
-
const c = data.citations[i];
|
|
479
|
-
if (c) {
|
|
480
|
-
lines.push(`**[${i + 1}]** \`${c.uri}\``);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
lines.push("");
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Show all retrieved sources if no answer or --show-sources
|
|
487
|
-
const showAllSources = !hasAnswer || opts.showSources;
|
|
488
|
-
if (showAllSources) {
|
|
489
|
-
lines.push(hasAnswer ? "## All Retrieved Sources" : "## Sources");
|
|
490
|
-
lines.push("");
|
|
491
|
-
|
|
492
|
-
for (let i = 0; i < data.results.length; i++) {
|
|
493
|
-
const r = data.results[i];
|
|
494
|
-
if (!r) {
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
lines.push(`${i + 1}. **${r.title || r.source.relPath}**`);
|
|
498
|
-
lines.push(` - URI: \`${r.uri}\``);
|
|
499
|
-
lines.push(` - Score: ${r.score.toFixed(2)}`);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (data.results.length === 0) {
|
|
503
|
-
lines.push("*No relevant sources found.*");
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
lines.push("");
|
|
508
|
-
lines.push("---");
|
|
509
|
-
lines.push(
|
|
510
|
-
`*Mode: ${data.mode} | Expanded: ${data.meta.expanded} | Reranked: ${data.meta.reranked}*`
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
return lines.join("\n");
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Format ask result for output.
|
|
518
|
-
*/
|
|
519
|
-
export function formatAsk(
|
|
520
|
-
result: AskCommandResult,
|
|
521
|
-
options: AskCommandOptions
|
|
522
|
-
): string {
|
|
523
|
-
if (!result.success) {
|
|
524
|
-
return options.json
|
|
525
|
-
? JSON.stringify({
|
|
526
|
-
error: { code: "ASK_FAILED", message: result.error },
|
|
527
|
-
})
|
|
528
|
-
: `Error: ${result.error}`;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const formatOpts: FormatOptions = { showSources: options.showSources };
|
|
532
|
-
|
|
533
|
-
if (options.json) {
|
|
534
|
-
return JSON.stringify(result.data, null, 2);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (options.md) {
|
|
538
|
-
return formatMarkdown(result.data, formatOpts);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return formatTerminal(result.data, formatOpts);
|
|
542
|
-
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** CLI adapters and readable formatters for knowledge delta read services. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
KnowledgeChangesResult,
|
|
5
|
+
KnowledgeDeltaServiceResult,
|
|
6
|
+
KnowledgeDiffResult,
|
|
7
|
+
KnowledgeImpactInput,
|
|
8
|
+
KnowledgeImpactResult,
|
|
9
|
+
ListKnowledgeChangesInput,
|
|
10
|
+
} from "../../core/knowledge-delta";
|
|
11
|
+
import type { StorePort } from "../../store/types";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
analyzeKnowledgeImpact,
|
|
15
|
+
getKnowledgeDiff,
|
|
16
|
+
listKnowledgeChanges,
|
|
17
|
+
} from "../../core/knowledge-delta";
|
|
18
|
+
import { initStore } from "./shared";
|
|
19
|
+
|
|
20
|
+
export interface KnowledgeDeltaCliContext {
|
|
21
|
+
configPath?: string;
|
|
22
|
+
indexName?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const changesRead = (
|
|
26
|
+
store: StorePort,
|
|
27
|
+
input: ListKnowledgeChangesInput = {}
|
|
28
|
+
): Promise<KnowledgeDeltaServiceResult<KnowledgeChangesResult>> =>
|
|
29
|
+
listKnowledgeChanges(store, input);
|
|
30
|
+
|
|
31
|
+
export const diffRead = (
|
|
32
|
+
store: StorePort,
|
|
33
|
+
ref: string,
|
|
34
|
+
changeId?: string
|
|
35
|
+
): Promise<KnowledgeDeltaServiceResult<KnowledgeDiffResult>> =>
|
|
36
|
+
getKnowledgeDiff(store, ref, changeId);
|
|
37
|
+
|
|
38
|
+
export const impactRead = (
|
|
39
|
+
store: StorePort,
|
|
40
|
+
ref: string,
|
|
41
|
+
input: KnowledgeImpactInput = {}
|
|
42
|
+
): Promise<KnowledgeDeltaServiceResult<KnowledgeImpactResult>> =>
|
|
43
|
+
analyzeKnowledgeImpact(store, ref, input);
|
|
44
|
+
|
|
45
|
+
const withStore = async <T>(
|
|
46
|
+
context: KnowledgeDeltaCliContext,
|
|
47
|
+
operation: (store: StorePort) => Promise<KnowledgeDeltaServiceResult<T>>
|
|
48
|
+
): Promise<KnowledgeDeltaServiceResult<T>> => {
|
|
49
|
+
const initialized = await initStore({
|
|
50
|
+
configPath: context.configPath,
|
|
51
|
+
indexName: context.indexName,
|
|
52
|
+
syncConfig: false,
|
|
53
|
+
allowEmptyCollections: true,
|
|
54
|
+
});
|
|
55
|
+
if (!initialized.ok) {
|
|
56
|
+
return { success: false, error: initialized.error };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return await operation(initialized.store);
|
|
60
|
+
} finally {
|
|
61
|
+
await initialized.store.close();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const changes = (
|
|
66
|
+
input: ListKnowledgeChangesInput,
|
|
67
|
+
context: KnowledgeDeltaCliContext = {}
|
|
68
|
+
): Promise<KnowledgeDeltaServiceResult<KnowledgeChangesResult>> =>
|
|
69
|
+
withStore(context, (store) => changesRead(store, input));
|
|
70
|
+
|
|
71
|
+
export const diff = (
|
|
72
|
+
ref: string,
|
|
73
|
+
changeId: string | undefined,
|
|
74
|
+
context: KnowledgeDeltaCliContext = {}
|
|
75
|
+
): Promise<KnowledgeDeltaServiceResult<KnowledgeDiffResult>> =>
|
|
76
|
+
withStore(context, (store) => diffRead(store, ref, changeId));
|
|
77
|
+
|
|
78
|
+
export const impact = (
|
|
79
|
+
ref: string,
|
|
80
|
+
input: KnowledgeImpactInput,
|
|
81
|
+
context: KnowledgeDeltaCliContext = {}
|
|
82
|
+
): Promise<KnowledgeDeltaServiceResult<KnowledgeImpactResult>> =>
|
|
83
|
+
withStore(context, (store) => impactRead(store, ref, input));
|
|
84
|
+
|
|
85
|
+
const json = (value: unknown): string => JSON.stringify(value, null, 2);
|
|
86
|
+
|
|
87
|
+
export function formatChanges(
|
|
88
|
+
result: KnowledgeChangesResult,
|
|
89
|
+
format: "terminal" | "json"
|
|
90
|
+
): string {
|
|
91
|
+
if (format === "json") return json(result);
|
|
92
|
+
const lines = [`${result.changes.length} retained changes`];
|
|
93
|
+
for (const change of result.changes) {
|
|
94
|
+
const uri = change.current?.uri ?? change.previous?.uri ?? "(unknown)";
|
|
95
|
+
lines.push(
|
|
96
|
+
`${change.observedAt} ${change.kind.padEnd(10)} ${uri} ${change.id}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (result.page.cursorExpired) {
|
|
100
|
+
lines.push(`Cursor expired; earliest: ${result.page.earliestCursor}`);
|
|
101
|
+
} else if (result.page.nextCursor) {
|
|
102
|
+
lines.push(`Next cursor: ${result.page.nextCursor}`);
|
|
103
|
+
}
|
|
104
|
+
for (const warning of result.warnings) {
|
|
105
|
+
lines.push(`Warning: ${warning}`);
|
|
106
|
+
}
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatDiff(
|
|
111
|
+
result: KnowledgeDiffResult,
|
|
112
|
+
format: "terminal" | "json"
|
|
113
|
+
): string {
|
|
114
|
+
if (format === "json") return json(result);
|
|
115
|
+
const lines = [
|
|
116
|
+
`Structural diff for ${result.document.uri}`,
|
|
117
|
+
`Status: ${result.status}`,
|
|
118
|
+
`History: ${result.history.status}${result.history.reason ? ` (${result.history.reason})` : ""}`,
|
|
119
|
+
"Content: not retained (metadata-only journal)",
|
|
120
|
+
];
|
|
121
|
+
if (result.change) {
|
|
122
|
+
const delta = result.change.structureDelta;
|
|
123
|
+
lines.push(
|
|
124
|
+
`Change: ${result.change.kind} at ${result.change.observedAt} (${result.change.id})`,
|
|
125
|
+
`Headings: +${delta.headings.added.length} -${delta.headings.removed.length}`,
|
|
126
|
+
`Links: +${delta.links.added.length} -${delta.links.removed.length}`,
|
|
127
|
+
`Typed edges: +${delta.typedEdges.added.length} -${delta.typedEdges.removed.length}`,
|
|
128
|
+
`Dates: +${delta.dates.added.length} -${delta.dates.removed.length} ~${delta.dates.changed.length}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
for (const warning of result.warnings) {
|
|
132
|
+
lines.push(`Warning: ${warning}`);
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function formatImpact(
|
|
138
|
+
result: KnowledgeImpactResult,
|
|
139
|
+
format: "terminal" | "json"
|
|
140
|
+
): string {
|
|
141
|
+
if (format === "json") return json(result);
|
|
142
|
+
const lines = [
|
|
143
|
+
`${result.impacted.length} documents depend on ${result.root.uri}`,
|
|
144
|
+
];
|
|
145
|
+
for (const item of result.impacted) {
|
|
146
|
+
const path = item.evidencePath
|
|
147
|
+
.map(
|
|
148
|
+
(step) => `${step.source.uri} -[${step.edgeType}]-> ${step.target.uri}`
|
|
149
|
+
)
|
|
150
|
+
.join(" -> ");
|
|
151
|
+
lines.push(`depth ${item.depth} ${item.document.uri}`, ` ${path}`);
|
|
152
|
+
}
|
|
153
|
+
if (result.meta.truncated) {
|
|
154
|
+
lines.push("Traversal truncated by configured caps");
|
|
155
|
+
}
|
|
156
|
+
for (const warning of result.meta.warnings) {
|
|
157
|
+
lines.push(`Warning: ${warning}`);
|
|
158
|
+
}
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/** CLI lifecycle for explicitly saved Context Capsules. */
|
|
2
|
+
|
|
3
|
+
import type { Config } from "../../config/types";
|
|
4
|
+
import type { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
5
|
+
import type { SavedCapsuleRegistrationRecord } from "../../store/types";
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_INDEX_NAME } from "../../app/constants";
|
|
8
|
+
import { canonicalizeIndexName } from "../../app/index-name";
|
|
9
|
+
import {
|
|
10
|
+
canonicalSavedCapsuleRegistryJson,
|
|
11
|
+
listSavedCapsules,
|
|
12
|
+
loadSavedCapsuleFile,
|
|
13
|
+
registerSavedCapsule,
|
|
14
|
+
unregisterSavedCapsule,
|
|
15
|
+
} from "../../core/capsule-registry";
|
|
16
|
+
import { reverifySavedCapsuleManually } from "../../core/capsule-reverification";
|
|
17
|
+
import { CliError } from "../errors";
|
|
18
|
+
import { contextCliError } from "./context-build";
|
|
19
|
+
import { initStore } from "./shared";
|
|
20
|
+
|
|
21
|
+
export interface SavedCapsuleCommandOptions {
|
|
22
|
+
configPath?: string;
|
|
23
|
+
indexName?: string;
|
|
24
|
+
format: "terminal" | "json";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WatchSavedCapsuleCommandOptions extends SavedCapsuleCommandOptions {
|
|
28
|
+
explicitIndexName?: string;
|
|
29
|
+
question?: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
notify?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ReverifySavedCapsuleCommandResult {
|
|
35
|
+
output: string;
|
|
36
|
+
operationStatus: "completed" | "failed";
|
|
37
|
+
errorMessage: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const withStore = async <T>(
|
|
41
|
+
options: SavedCapsuleCommandOptions,
|
|
42
|
+
operation: (input: {
|
|
43
|
+
store: SqliteAdapter;
|
|
44
|
+
config: Config;
|
|
45
|
+
indexName: string;
|
|
46
|
+
}) => Promise<T>
|
|
47
|
+
): Promise<T> => {
|
|
48
|
+
const indexName = canonicalizeIndexName(
|
|
49
|
+
options.indexName ?? DEFAULT_INDEX_NAME
|
|
50
|
+
);
|
|
51
|
+
const initialized = await initStore({
|
|
52
|
+
configPath: options.configPath,
|
|
53
|
+
indexName,
|
|
54
|
+
syncConfig: true,
|
|
55
|
+
allowEmptyCollections: true,
|
|
56
|
+
});
|
|
57
|
+
if (!initialized.ok) throw new CliError("RUNTIME", initialized.error);
|
|
58
|
+
try {
|
|
59
|
+
return await operation({
|
|
60
|
+
store: initialized.store,
|
|
61
|
+
config: initialized.config,
|
|
62
|
+
indexName,
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
await initialized.store.close();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const formatRegistration = (
|
|
70
|
+
registration: SavedCapsuleRegistrationRecord
|
|
71
|
+
): string =>
|
|
72
|
+
[
|
|
73
|
+
`${registration.registrationId} ${registration.label ?? registration.capsuleId}`,
|
|
74
|
+
` file: ${registration.filePath}`,
|
|
75
|
+
` index: ${registration.indexName}`,
|
|
76
|
+
` evidence: ${registration.evidence.length}`,
|
|
77
|
+
` notify: ${registration.notificationPreference}`,
|
|
78
|
+
` state: ${registration.verification?.affectedQuestionState ?? "not_verified"}`,
|
|
79
|
+
].join("\n");
|
|
80
|
+
|
|
81
|
+
export const watchSavedCapsule = async (
|
|
82
|
+
filePath: string,
|
|
83
|
+
options: WatchSavedCapsuleCommandOptions
|
|
84
|
+
): Promise<string> => {
|
|
85
|
+
try {
|
|
86
|
+
const loaded = await loadSavedCapsuleFile(filePath);
|
|
87
|
+
const explicit = options.explicitIndexName
|
|
88
|
+
? canonicalizeIndexName(options.explicitIndexName)
|
|
89
|
+
: undefined;
|
|
90
|
+
if (explicit && explicit !== loaded.capsule.scope.indexName) {
|
|
91
|
+
throw Object.assign(
|
|
92
|
+
new Error(
|
|
93
|
+
`Context Capsule index ${loaded.capsule.scope.indexName} does not match --index ${explicit}`
|
|
94
|
+
),
|
|
95
|
+
{ code: "invalid_filter" }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return await withStore(
|
|
99
|
+
{ ...options, indexName: loaded.capsule.scope.indexName },
|
|
100
|
+
async ({ store, indexName }) => {
|
|
101
|
+
const registration = await registerSavedCapsule(store, indexName, {
|
|
102
|
+
filePath,
|
|
103
|
+
question: options.question,
|
|
104
|
+
label: options.label,
|
|
105
|
+
notificationPreference: options.notify ? "local" : "none",
|
|
106
|
+
});
|
|
107
|
+
return options.format === "json"
|
|
108
|
+
? canonicalSavedCapsuleRegistryJson(registration)
|
|
109
|
+
: formatRegistration(registration);
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw contextCliError(error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const listWatchedCapsules = async (
|
|
118
|
+
options: SavedCapsuleCommandOptions
|
|
119
|
+
): Promise<string> =>
|
|
120
|
+
withStore(options, async ({ store }) => {
|
|
121
|
+
const registrations = await listSavedCapsules(store);
|
|
122
|
+
if (options.format === "json") {
|
|
123
|
+
return canonicalSavedCapsuleRegistryJson({
|
|
124
|
+
schemaVersion: "1.0",
|
|
125
|
+
registrations,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return registrations.length === 0
|
|
129
|
+
? "No saved Context Capsules are watched."
|
|
130
|
+
: registrations.map(formatRegistration).join("\n\n");
|
|
131
|
+
}).catch((error) => {
|
|
132
|
+
throw contextCliError(error);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const unwatchSavedCapsule = async (
|
|
136
|
+
registrationId: string,
|
|
137
|
+
options: SavedCapsuleCommandOptions
|
|
138
|
+
): Promise<string> =>
|
|
139
|
+
withStore(options, async ({ store }) => {
|
|
140
|
+
await unregisterSavedCapsule(store, registrationId);
|
|
141
|
+
return options.format === "json"
|
|
142
|
+
? canonicalSavedCapsuleRegistryJson({
|
|
143
|
+
schemaVersion: "1.0",
|
|
144
|
+
registrationId,
|
|
145
|
+
removed: true,
|
|
146
|
+
})
|
|
147
|
+
: `Stopped watching ${registrationId}.`;
|
|
148
|
+
}).catch((error) => {
|
|
149
|
+
throw contextCliError(error);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const reverifyWatchedCapsule = async (
|
|
153
|
+
registrationId: string,
|
|
154
|
+
options: SavedCapsuleCommandOptions
|
|
155
|
+
): Promise<ReverifySavedCapsuleCommandResult> =>
|
|
156
|
+
withStore(options, async ({ store, config, indexName }) => {
|
|
157
|
+
const outcome = await reverifySavedCapsuleManually(registrationId, {
|
|
158
|
+
store,
|
|
159
|
+
config,
|
|
160
|
+
indexName,
|
|
161
|
+
});
|
|
162
|
+
const errorMessage =
|
|
163
|
+
outcome.verification.operationStatus === "failed"
|
|
164
|
+
? `${outcome.verification.errorCode ?? "verification_failed"}: ${
|
|
165
|
+
outcome.verification.errorMessage ??
|
|
166
|
+
"Saved Context Capsule verification failed"
|
|
167
|
+
}`
|
|
168
|
+
: null;
|
|
169
|
+
const output =
|
|
170
|
+
options.format === "json"
|
|
171
|
+
? canonicalSavedCapsuleRegistryJson({
|
|
172
|
+
schemaVersion: "1.0",
|
|
173
|
+
registration: outcome.registration,
|
|
174
|
+
verification: outcome.verification,
|
|
175
|
+
receipt: outcome.receipt,
|
|
176
|
+
})
|
|
177
|
+
: [
|
|
178
|
+
formatRegistration(outcome.registration),
|
|
179
|
+
` operation: ${outcome.verification.operationStatus}`,
|
|
180
|
+
...(errorMessage ? [` error: ${errorMessage}`] : []),
|
|
181
|
+
].join("\n");
|
|
182
|
+
return {
|
|
183
|
+
output,
|
|
184
|
+
operationStatus: outcome.verification.operationStatus,
|
|
185
|
+
errorMessage,
|
|
186
|
+
};
|
|
187
|
+
}).catch((error) => {
|
|
188
|
+
throw contextCliError(error);
|
|
189
|
+
});
|
package/src/cli/options.ts
CHANGED
|
@@ -35,6 +35,7 @@ export const CMD = {
|
|
|
35
35
|
contextCheck: "context.check",
|
|
36
36
|
contextBuild: "context.build",
|
|
37
37
|
contextVerify: "context.verify",
|
|
38
|
+
contextSaved: "context.saved",
|
|
38
39
|
modelsList: "models.list",
|
|
39
40
|
tagsList: "tags.list",
|
|
40
41
|
linksList: "links.list",
|
|
@@ -42,6 +43,9 @@ export const CMD = {
|
|
|
42
43
|
similar: "similar",
|
|
43
44
|
graph: "graph",
|
|
44
45
|
graphQuery: "graph.query",
|
|
46
|
+
changes: "changes",
|
|
47
|
+
diff: "diff",
|
|
48
|
+
impact: "impact",
|
|
45
49
|
capture: "capture",
|
|
46
50
|
} as const;
|
|
47
51
|
|
|
@@ -64,6 +68,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
|
|
|
64
68
|
[CMD.contextCheck]: ["terminal", "json", "md"],
|
|
65
69
|
[CMD.contextBuild]: ["terminal", "json", "md"],
|
|
66
70
|
[CMD.contextVerify]: ["terminal", "json", "md"],
|
|
71
|
+
[CMD.contextSaved]: ["terminal", "json"],
|
|
67
72
|
[CMD.modelsList]: ["terminal", "json"],
|
|
68
73
|
[CMD.tagsList]: ["terminal", "json", "md"],
|
|
69
74
|
[CMD.linksList]: ["terminal", "json", "md"],
|
|
@@ -72,6 +77,9 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
|
|
|
72
77
|
// graph uses custom --dot/--mermaid flags (not OutputFormat) and writes via terminal output
|
|
73
78
|
[CMD.graph]: ["json", "terminal"],
|
|
74
79
|
[CMD.graphQuery]: ["terminal", "json"],
|
|
80
|
+
[CMD.changes]: ["terminal", "json"],
|
|
81
|
+
[CMD.diff]: ["terminal", "json"],
|
|
82
|
+
[CMD.impact]: ["terminal", "json"],
|
|
75
83
|
[CMD.capture]: ["terminal", "json"],
|
|
76
84
|
};
|
|
77
85
|
|