@gmickel/gno 2.10.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +11 -11
  2. package/assets/skill/SKILL.md +25 -14
  3. package/assets/skill/cli-reference.md +6 -5
  4. package/assets/skill/examples.md +16 -12
  5. package/assets/skill/mcp-reference.md +2 -1
  6. package/browser-extension/artifacts/{gno-browser-clipper-v2.10.1.zip → gno-browser-clipper-v2.11.0.zip} +0 -0
  7. package/browser-extension/artifacts/gno-browser-clipper-v2.11.0.zip.sha256 +1 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/package.json +3 -1
  10. package/spec/cli.md +27 -1
  11. package/spec/mcp.md +27 -3
  12. package/spec/output-schemas/ask.schema.json +8 -0
  13. package/spec/output-schemas/claim-verification.schema.json +8 -2
  14. package/spec/output-schemas/context-capsule-v1.schema.json +6 -1
  15. package/spec/output-schemas/context-capsule-verification.schema.json +4 -2
  16. package/spec/output-schemas/get.schema.json +4 -0
  17. package/spec/output-schemas/metadata-summary.schema.json +60 -0
  18. package/spec/output-schemas/multi-get.schema.json +4 -0
  19. package/spec/output-schemas/search-result.schema.json +4 -0
  20. package/spec/output-schemas/search-results.schema.json +4 -0
  21. package/src/app/constants.ts +16 -0
  22. package/src/app/context-format.ts +1 -0
  23. package/src/app/verified-ask.ts +2 -0
  24. package/src/cli/commands/agents/block.ts +9 -8
  25. package/src/cli/commands/ask-format.ts +18 -1
  26. package/src/cli/commands/ask.ts +4 -5
  27. package/src/cli/commands/daemon.ts +5 -1
  28. package/src/cli/commands/get.ts +11 -1
  29. package/src/cli/commands/metadata.ts +79 -0
  30. package/src/cli/commands/multi-get.ts +13 -0
  31. package/src/cli/commands/shared.ts +9 -7
  32. package/src/cli/format/search-results.ts +11 -0
  33. package/src/cli/options.ts +2 -0
  34. package/src/cli/program.ts +45 -0
  35. package/src/core/context-capsule-schema.ts +2 -0
  36. package/src/core/context-capsule-verification.ts +2 -0
  37. package/src/core/context-compiler.ts +2 -5
  38. package/src/core/context-evidence.ts +1 -0
  39. package/src/core/context-verifier.ts +14 -1
  40. package/src/core/metadata-discovery.ts +169 -0
  41. package/src/ingestion/sync.ts +49 -1
  42. package/src/ingestion/walker.ts +11 -3
  43. package/src/mcp/http-egress.ts +1 -0
  44. package/src/mcp/tool-descriptions-core.ts +2 -2
  45. package/src/mcp/tool-profile.ts +16 -1
  46. package/src/mcp/tools/get.ts +10 -0
  47. package/src/mcp/tools/index.ts +39 -4
  48. package/src/mcp/tools/multi-get.ts +15 -0
  49. package/src/mcp/tools/query.ts +5 -3
  50. package/src/mcp/tools/search.ts +5 -3
  51. package/src/mcp/tools/vsearch.ts +5 -3
  52. package/src/mcp/tools/workspace-write.ts +42 -34
  53. package/src/pipeline/answer-prompt.ts +15 -2
  54. package/src/pipeline/answer.ts +3 -0
  55. package/src/pipeline/claim-verification-schema.ts +6 -1
  56. package/src/pipeline/claim-verification.ts +54 -1
  57. package/src/pipeline/explain.ts +1 -1
  58. package/src/pipeline/fusion.ts +24 -4
  59. package/src/pipeline/hybrid.ts +5 -1
  60. package/src/pipeline/result-context.ts +29 -0
  61. package/src/pipeline/search.ts +5 -1
  62. package/src/pipeline/types.ts +4 -0
  63. package/src/pipeline/vsearch.ts +5 -1
  64. package/src/sdk/client.ts +32 -4
  65. package/src/sdk/documents.ts +5 -0
  66. package/src/sdk/types.ts +5 -0
  67. package/src/serve/routes/api.ts +29 -1
  68. package/src/serve/server.ts +12 -0
  69. package/src/store/sqlite/adapter.ts +82 -2
  70. package/src/store/types.ts +16 -0
  71. package/src/store/vector/variant-search.ts +112 -74
  72. package/browser-extension/artifacts/gno-browser-clipper-v2.10.1.zip.sha256 +0 -1
@@ -146,6 +146,9 @@ const formatTerminal = (data: AskResult, opts: FormatOptions = {}): string => {
146
146
  ? ""
147
147
  : `:L${citation.startLine}${citation.endLine === undefined || citation.endLine === citation.startLine ? "" : `-L${citation.endLine}`}`;
148
148
  lines.push(` [${index + 1}] ${citation.uri}${range}`);
149
+ if (citation.supersededBy) {
150
+ lines.push(` Superseded by: ${citation.supersededBy}`);
151
+ }
149
152
  }
150
153
  lines.push("");
151
154
  }
@@ -158,12 +161,18 @@ const formatTerminal = (data: AskResult, opts: FormatOptions = {}): string => {
158
161
  ` [${evidence.docid}] ${exactSpan(evidence.uri, evidence.startLine, evidence.endLine)}`
159
162
  );
160
163
  if (evidence.title) lines.push(` ${evidence.title}`);
164
+ if (evidence.supersededBy) {
165
+ lines.push(` Superseded by: ${evidence.supersededBy}`);
166
+ }
161
167
  }
162
168
  } else if (showAllSources && data.results.length > 0) {
163
169
  lines.push(hasAnswer ? "All Retrieved Sources:" : "Sources:");
164
170
  for (const result of data.results) {
165
171
  lines.push(` [${result.docid}] ${result.uri}`);
166
172
  if (result.title) lines.push(` ${result.title}`);
173
+ if (result.supersededBy) {
174
+ lines.push(` Superseded by: ${result.supersededBy}`);
175
+ }
167
176
  }
168
177
  } else if (hasAnswer && data.results.length > 0) {
169
178
  const citedCount = data.citations?.length ?? 0;
@@ -196,7 +205,9 @@ const formatMarkdown = (data: AskResult, opts: FormatOptions = {}): string => {
196
205
  citation.startLine === undefined
197
206
  ? ""
198
207
  : `:L${citation.startLine}${citation.endLine === undefined || citation.endLine === citation.startLine ? "" : `-L${citation.endLine}`}`;
199
- lines.push(`**[${index + 1}]** \`${citation.uri}${range}\``);
208
+ lines.push(
209
+ `**[${index + 1}]** \`${citation.uri}${range}\`${citation.supersededBy ? ` (superseded by \`${citation.supersededBy}\`)` : ""}`
210
+ );
200
211
  }
201
212
  lines.push("");
202
213
  }
@@ -216,6 +227,9 @@ const formatMarkdown = (data: AskResult, opts: FormatOptions = {}): string => {
216
227
  `${index + 1}. **${evidence.title || evidence.uri}**`,
217
228
  ` - URI: \`${exactSpan(evidence.uri, evidence.startLine, evidence.endLine)}\``
218
229
  );
230
+ if (evidence.supersededBy) {
231
+ lines.push(` - Superseded by: \`${evidence.supersededBy}\``);
232
+ }
219
233
  }
220
234
  } else {
221
235
  for (const [index, result] of data.results.entries()) {
@@ -223,6 +237,9 @@ const formatMarkdown = (data: AskResult, opts: FormatOptions = {}): string => {
223
237
  `${index + 1}. **${result.title || result.source.relPath}**`
224
238
  );
225
239
  lines.push(` - URI: \`${result.uri}\``);
240
+ if (result.supersededBy) {
241
+ lines.push(` - Superseded by: \`${result.supersededBy}\``);
242
+ }
226
243
  lines.push(` - Score: ${result.score.toFixed(2)}`);
227
244
  }
228
245
  if (data.results.length === 0) lines.push("*No relevant sources found.*");
@@ -15,7 +15,7 @@ import type {
15
15
  } from "../../llm/types";
16
16
  import type { AskOptions, AskResult, Citation } from "../../pipeline/types";
17
17
 
18
- import { decorateUriForIndex } from "../../app/constants";
18
+ import { decorateResultUris } from "../../app/constants";
19
19
  import { buildVerifiedAsk } from "../../app/verified-ask";
20
20
  import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
21
21
  import {
@@ -89,10 +89,9 @@ export function decorateAskUris(
89
89
  result: AskResult,
90
90
  indexName: string | undefined
91
91
  ): AskResult {
92
- const decorate = <T extends { uri: string }>(item: T): T => ({
93
- ...item,
94
- uri: decorateUriForIndex(item.uri, indexName),
95
- });
92
+ const decorate = <T extends { uri: string; supersededBy?: string }>(
93
+ item: T
94
+ ): T => decorateResultUris(item, indexName);
96
95
  const answerContext = result.meta.answerContext;
97
96
  return {
98
97
  ...result,
@@ -99,7 +99,11 @@ export function logSessionAutomationResult(
99
99
  }
100
100
 
101
101
  function formatCollectionSyncSummary(result: CollectionSyncResult): string {
102
- return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors`;
102
+ const collectionErrors = result.errors
103
+ .filter((error) => error.relPath === "")
104
+ .map((error) => `; [${error.code}] ${error.message}`)
105
+ .join("");
106
+ return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors${collectionErrors}`;
103
107
  }
104
108
 
105
109
  export const authorizeDaemonStatus = async (
@@ -29,6 +29,7 @@ import {
29
29
  evidenceFromExactDocument,
30
30
  RetrievalTraceSession as TraceSession,
31
31
  } from "../../core/retrieval-trace-session";
32
+ import { supersedingUri } from "../../pipeline/result-context";
32
33
  import { parseRef } from "./ref-parser";
33
34
  import { initStore } from "./shared";
34
35
 
@@ -90,6 +91,8 @@ export interface GetResponse {
90
91
  mirrorHash?: string;
91
92
  };
92
93
  record?: RecordEvidenceMetadata;
94
+ /** URI of the current fact when a newer memory fact superseded this one. */
95
+ supersededBy?: string;
93
96
  capabilities: DocumentCapabilities;
94
97
  }
95
98
 
@@ -269,7 +272,7 @@ async function fetchDocument(
269
272
  return { success: false, error: "Mirror content unavailable" };
270
273
  }
271
274
 
272
- return buildResponse({
275
+ const response = buildResponse({
273
276
  doc,
274
277
  fullContent: contentResult.value,
275
278
  parsed,
@@ -277,6 +280,10 @@ async function fetchDocument(
277
280
  config,
278
281
  indexName,
279
282
  });
283
+ const successor = await supersedingUri(store, doc.uri);
284
+ if (response.success && successor)
285
+ response.data.supersededBy = decorateUriForIndex(successor, indexName);
286
+ return response;
280
287
  }
281
288
 
282
289
  interface BuildResponseContext {
@@ -467,6 +474,9 @@ function formatMarkdown(data: GetResponse, options: GetCommandOptions): string {
467
474
  lines.push("");
468
475
  lines.push(`- **URI**: \`${data.uri}\``);
469
476
  lines.push(`- **DocID**: \`${data.docid}\``);
477
+ if (data.supersededBy) {
478
+ lines.push(`- **Superseded by**: \`${data.supersededBy}\``);
479
+ }
470
480
  if (data.returnedLines) {
471
481
  lines.push(
472
482
  `- **Lines**: ${data.returnedLines.start}-${data.returnedLines.end} of ${data.totalLines}`
@@ -0,0 +1,79 @@
1
+ /**
2
+ * gno metadata - list the typed metadata keys indexed documents carry.
3
+ *
4
+ * @module src/cli/commands/metadata
5
+ */
6
+
7
+ import {
8
+ formatMetadataSummary,
9
+ type MetadataSummary,
10
+ readMetadataSummary,
11
+ } from "../../core/metadata-discovery";
12
+ import { initStore } from "./shared";
13
+
14
+ export interface MetadataListOptions {
15
+ configPath?: string;
16
+ indexName?: string;
17
+ collection?: string;
18
+ values?: number;
19
+ }
20
+
21
+ export type MetadataListResult =
22
+ | { success: true; data: MetadataSummary }
23
+ | { success: false; error: string; isValidation?: boolean };
24
+
25
+ export async function metadataList(
26
+ options: MetadataListOptions = {}
27
+ ): Promise<MetadataListResult> {
28
+ const initResult = await initStore({
29
+ configPath: options.configPath,
30
+ indexName: options.indexName,
31
+ });
32
+ if (!initResult.ok) return { success: false, error: initResult.error };
33
+ const { store } = initResult;
34
+ try {
35
+ return {
36
+ success: true,
37
+ data: await readMetadataSummary(store, {
38
+ collection: options.collection,
39
+ values: options.values,
40
+ }),
41
+ };
42
+ } catch (cause) {
43
+ return {
44
+ success: false,
45
+ error: cause instanceof Error ? cause.message : String(cause),
46
+ isValidation: cause instanceof RangeError,
47
+ };
48
+ } finally {
49
+ await store.close();
50
+ }
51
+ }
52
+
53
+ export function formatMetadataList(
54
+ data: MetadataSummary,
55
+ options: { json?: boolean; md?: boolean }
56
+ ): string {
57
+ if (options.json) return JSON.stringify(data, null, 2);
58
+ if (!options.md) return formatMetadataSummary(data);
59
+ const lines = [
60
+ "# Typed metadata",
61
+ "",
62
+ `*${data.documentsWithMetadata} of ${data.documents} documents${data.collection ? ` in \`${data.collection}\`` : ""}*`,
63
+ ];
64
+ if (data.keys.length === 0) return lines.join("\n");
65
+ lines.push(
66
+ "",
67
+ "| Key | Types | Documents | Common values |",
68
+ "|---|---|---|---|"
69
+ );
70
+ for (const key of data.keys) {
71
+ const values = key.values
72
+ .map((item) => `\`${JSON.stringify(item.value)}\` (${item.documents})`)
73
+ .join(", ");
74
+ lines.push(
75
+ `| \`${key.key}\` | ${key.types.join(", ")} | ${key.documents} | ${values}${key.valuesTruncated ? ", ..." : ""} |`
76
+ );
77
+ }
78
+ return lines.join("\n");
79
+ }
@@ -21,6 +21,7 @@ import {
21
21
  projectRecordEvidenceMetadata,
22
22
  type RecordEvidenceMetadata,
23
23
  } from "../../core/record-metadata";
24
+ import { supersedingUri } from "../../pipeline/result-context";
24
25
  import { isGlobPattern, parseRef, splitRefs } from "./ref-parser";
25
26
  import { initStore } from "./shared";
26
27
 
@@ -58,6 +59,8 @@ export interface MultiGetDocument {
58
59
  totalLines?: number;
59
60
  source: { absPath?: string; relPath: string; mime: string; ext: string };
60
61
  record?: RecordEvidenceMetadata;
62
+ /** URI of the current fact when a newer memory fact superseded this one. */
63
+ supersededBy?: string;
61
64
  }
62
65
 
63
66
  export interface SkippedDoc {
@@ -236,7 +239,11 @@ async function fetchSingleDocument(
236
239
  const coll = ctx.config.collections.find((c) => c.name === doc.collection);
237
240
 
238
241
  const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
242
+ const successor = await supersedingUri(ctx.store, doc.uri);
239
243
  ctx.documents.push({
244
+ ...(successor
245
+ ? { supersededBy: decorateUriForIndex(successor, ctx.indexName) }
246
+ : {}),
240
247
  docid: doc.docid,
241
248
  uri: decorateUriForIndex(doc.uri, ctx.indexName),
242
249
  title: doc.title ?? undefined,
@@ -422,6 +429,9 @@ function formatMarkdown(
422
429
  for (const doc of data.documents) {
423
430
  lines.push(`## ${doc.title || doc.source.relPath}`);
424
431
  lines.push(`- **URI**: \`${doc.uri}\``);
432
+ if (doc.supersededBy) {
433
+ lines.push(`- **Superseded by**: \`${doc.supersededBy}\``);
434
+ }
425
435
  if (doc.truncated) {
426
436
  lines.push(`- **Truncated**: yes (max ${data.meta.maxBytes} bytes)`);
427
437
  }
@@ -450,6 +460,9 @@ function formatTerminal(
450
460
 
451
461
  for (const doc of data.documents) {
452
462
  lines.push(`=== ${doc.uri} ===`);
463
+ if (doc.supersededBy) {
464
+ lines.push(`Superseded by: ${doc.supersededBy}`);
465
+ }
453
466
  lines.push(formatContent(doc.content, Boolean(options.lineNumbers)));
454
467
  lines.push("");
455
468
  }
@@ -11,7 +11,7 @@ import type { SearchResults } from "../../pipeline/types";
11
11
 
12
12
  import {
13
13
  DEFAULT_INDEX_NAME,
14
- decorateUriForIndex,
14
+ decorateResultUris,
15
15
  getIndexDbPath,
16
16
  } from "../../app/constants";
17
17
  import {
@@ -164,10 +164,9 @@ export function decorateSearchResultsForIndex(
164
164
  }
165
165
  return {
166
166
  ...data,
167
- results: data.results.map((result) => ({
168
- ...result,
169
- uri: decorateUriForIndex(result.uri, indexName),
170
- })),
167
+ results: data.results.map((result) =>
168
+ decorateResultUris(result, indexName)
169
+ ),
171
170
  };
172
171
  }
173
172
 
@@ -217,10 +216,13 @@ export function formatSyncResultLines(
217
216
  lines.push(` ${c.filesErrored} errors`);
218
217
  }
219
218
  // A file stopped at its conversion budget is always named, not only in
220
- // verbose output: it stays unindexed until a later run finishes it.
219
+ // verbose output: it stays unindexed until a later run finishes it. So is
220
+ // a collection-level error such as an unavailable root.
221
221
  if (!options.verbose) {
222
222
  for (const err of c.errors) {
223
- if (BUDGET_ERROR_CODES.has(err.code)) {
223
+ if (err.relPath === "") {
224
+ lines.push(` [${err.code}] ${err.message}`);
225
+ } else if (BUDGET_ERROR_CODES.has(err.code)) {
224
226
  lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
225
227
  }
226
228
  }
@@ -88,6 +88,9 @@ function formatTerminal(data: SearchResults, options: FormatOptions): string {
88
88
  if (r.title) {
89
89
  lines.push(` ${r.title}`);
90
90
  }
91
+ if (r.supersededBy) {
92
+ lines.push(` Superseded by: ${r.supersededBy}`);
93
+ }
91
94
  if (r.snippet) {
92
95
  const content = options.full
93
96
  ? r.snippet
@@ -175,6 +178,9 @@ function formatMarkdown(data: SearchResults, options: FormatOptions): string {
175
178
  lines.push(`- **URI**: \`${r.uri}\``);
176
179
  lines.push(`- **Score**: ${r.score.toFixed(2)}`);
177
180
  lines.push(`- **DocID**: \`${r.docid}\``);
181
+ if (r.supersededBy) {
182
+ lines.push(`- **Superseded by**: \`${r.supersededBy}\``);
183
+ }
178
184
  if (r.snippet) {
179
185
  const content = options.full
180
186
  ? r.snippet
@@ -223,6 +229,11 @@ function formatXml(data: SearchResults, options: FormatOptions): string {
223
229
  lines.push(` <title>${escapeXml(r.title)}</title>`);
224
230
  }
225
231
  lines.push(` <relPath>${escapeXml(r.source.relPath)}</relPath>`);
232
+ if (r.supersededBy) {
233
+ lines.push(
234
+ ` <supersededBy>${escapeXml(r.supersededBy)}</supersededBy>`
235
+ );
236
+ }
226
237
  if (r.snippet) {
227
238
  const content = options.full
228
239
  ? r.snippet
@@ -82,6 +82,7 @@ export const CMD = {
82
82
  contextSaved: "context.saved",
83
83
  modelsList: "models.list",
84
84
  tagsList: "tags.list",
85
+ metadataList: "metadata",
85
86
  linksList: "links.list",
86
87
  backlinks: "backlinks",
87
88
  similar: "similar",
@@ -118,6 +119,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
118
119
  [CMD.contextSaved]: ["terminal", "json"],
119
120
  [CMD.modelsList]: ["terminal", "json"],
120
121
  [CMD.tagsList]: ["terminal", "json", "md"],
122
+ [CMD.metadataList]: ["terminal", "json", "md"],
121
123
  [CMD.linksList]: ["terminal", "json", "md"],
122
124
  [CMD.backlinks]: ["terminal", "json", "md"],
123
125
  [CMD.similar]: ["terminal", "json", "md"],
@@ -2481,6 +2481,11 @@ function wireRetrievalCommands(program: Command): void {
2481
2481
  );
2482
2482
  }
2483
2483
 
2484
+ // Terminal output is the raw content; the mark goes to stderr so
2485
+ // piping the document stays byte-exact.
2486
+ if (format === "terminal" && result.data.supersededBy) {
2487
+ process.stderr.write(`Superseded by: ${result.data.supersededBy}\n`);
2488
+ }
2484
2489
  process.stdout.write(
2485
2490
  `${formatGet(result, {
2486
2491
  lineNumbers: Boolean(cmdOpts.lineNumbers),
@@ -4184,6 +4189,46 @@ function wireAgentsCommands(program: Command): void {
4184
4189
  // ─────────────────────────────────────────────────────────────────────────────
4185
4190
 
4186
4191
  function wireTagsCommands(program: Command): void {
4192
+ program
4193
+ .command("metadata")
4194
+ .description(
4195
+ "List typed metadata keys with their types, document counts and common values"
4196
+ )
4197
+ .option("-c, --collection <name>", "only documents in this collection")
4198
+ .option(
4199
+ "--values <n>",
4200
+ "common values listed per key (1-100, default 10)",
4201
+ (value) => Number(value)
4202
+ )
4203
+ .option("--json", "JSON output")
4204
+ .option("--md", "Markdown output")
4205
+ .action(async (cmdOpts: Record<string, unknown>) => {
4206
+ const format = getFormat(cmdOpts);
4207
+ assertFormatSupported(CMD.metadataList, format);
4208
+ const globals = getGlobals();
4209
+ const { metadataList, formatMetadataList } =
4210
+ await import("./commands/metadata");
4211
+ const result = await metadataList({
4212
+ configPath: globals.config,
4213
+ indexName: globals.index,
4214
+ collection: cmdOpts.collection as string | undefined,
4215
+ values: cmdOpts.values as number | undefined,
4216
+ });
4217
+ if (!result.success) {
4218
+ throw new CliError(
4219
+ result.isValidation ? "VALIDATION" : "RUNTIME",
4220
+ result.error
4221
+ );
4222
+ }
4223
+ await writeOutput(
4224
+ formatMetadataList(result.data, {
4225
+ json: format === "json",
4226
+ md: format === "md",
4227
+ }),
4228
+ format
4229
+ );
4230
+ });
4231
+
4187
4232
  const tagsCmd = program.command("tags").description("Manage document tags");
4188
4233
 
4189
4234
  // tags list
@@ -365,6 +365,8 @@ export const contextCapsuleEvidenceSchema = z
365
365
  ]),
366
366
  egressLineage: egressLineageSchema.optional(),
367
367
  record: recordEvidenceMetadataSchema.optional(),
368
+ /** Current successor when a newer memory fact superseded this one. */
369
+ supersededBy: contextCapsuleGnoUriSchema.optional(),
368
370
  })
369
371
  .strict()
370
372
  .superRefine((value, context) => {
@@ -51,6 +51,7 @@ export const contextCapsuleVerificationEvidenceSchema = z
51
51
  "chunk_corrupt",
52
52
  "source_missing",
53
53
  "mirror_missing",
54
+ "superseded",
54
55
  ]),
55
56
  rankingStatus: z.enum(["unchanged", "reranked", "unavailable"]),
56
57
  rankingCode: z.enum([
@@ -82,6 +83,7 @@ export const contextCapsuleVerificationEvidenceSchema = z
82
83
  "passage_stale",
83
84
  "chunk_missing",
84
85
  "chunk_corrupt",
86
+ "superseded",
85
87
  ].includes(value.contentCode) &&
86
88
  hashes.every((hash) => hash !== null)) ||
87
89
  (value.contentStatus === "missing" &&
@@ -28,7 +28,7 @@ import type {
28
28
  import type { ContextConfiguredGuidance } from "./context-guidance";
29
29
  import type { MetadataPredicate } from "./typed-metadata";
30
30
 
31
- import { decorateUriForIndex } from "../app/constants";
31
+ import { decorateResultUris, decorateUriForIndex } from "../app/constants";
32
32
  import { canonicalizeIndexName } from "../app/index-name";
33
33
  import { resolveTemporalRange } from "../pipeline/temporal";
34
34
  import { attachSearchResultPlannerMetadata } from "../pipeline/trace-metadata";
@@ -383,10 +383,7 @@ export const planContextEvidence = async <T, P>(
383
383
  const decoratedResults = responses
384
384
  .flatMap((response) => response.results)
385
385
  .map((result) => {
386
- const decorated = {
387
- ...result,
388
- uri: decorateUriForIndex(result.uri, indexName),
389
- };
386
+ const decorated = decorateResultUris(result, indexName);
390
387
  const metadata = result[SEARCH_RESULT_PLANNER_METADATA];
391
388
  return metadata
392
389
  ? attachSearchResultPlannerMetadata(decorated, metadata)
@@ -502,6 +502,7 @@ export const materializeContextEvidenceCandidates = async (
502
502
  ...(projectRecordEvidenceMetadata(document)
503
503
  ? { record: projectRecordEvidenceMetadata(document) }
504
504
  : {}),
505
+ ...(result.supersededBy ? { supersededBy: result.supersededBy } : {}),
505
506
  },
506
507
  },
507
508
  };
@@ -11,6 +11,7 @@ import type { ContextCapsuleVerification } from "./context-capsule-verification"
11
11
 
12
12
  import { decorateUriForIndex, deriveDocid } from "../app/constants";
13
13
  import { chunkMatchesCanonicalContent } from "../pipeline/chunk-lookup";
14
+ import { ok } from "../store/types";
14
15
  import { type ContextCapsuleCreateOptions } from "./context-capsule";
15
16
  import { sha256Text } from "./context-capsule-validation";
16
17
  import {
@@ -35,6 +36,7 @@ type ContextVerifierStore = Pick<
35
36
  | "getCollections"
36
37
  | "getContexts"
37
38
  | "getDocumentsByDocids"
39
+ | "getSupersedingUris"
38
40
  > &
39
41
  Required<Pick<StorePort, "getContentBatch">>;
40
42
 
@@ -127,6 +129,8 @@ interface LoadedEvidence {
127
129
  chunksByHash: Map<string, ChunkRow[]>;
128
130
  documents: DocumentRow[];
129
131
  snapshot: ContextEvidenceSnapshot;
132
+ /** Current successor URI of evidence whose memory fact was superseded. */
133
+ successors: ReadonlyMap<string, string>;
130
134
  }
131
135
 
132
136
  const missingReceipt = (evidence: Evidence): EvidenceReceipt => ({
@@ -229,6 +233,8 @@ const verifyContent = (
229
233
  contentCode = "chunk_missing";
230
234
  } else if (!chunkValid) {
231
235
  contentCode = "chunk_corrupt";
236
+ } else if (loaded.successors.has(evidence.uri)) {
237
+ contentCode = "superseded";
232
238
  }
233
239
 
234
240
  return {
@@ -374,16 +380,23 @@ export const verifyContextCapsule = async (
374
380
  )
375
381
  ),
376
382
  ];
377
- const [documentResult, contentResult, chunkResult, rankResult] =
383
+ const [documentResult, contentResult, chunkResult, rankResult, successors] =
378
384
  await Promise.all([
379
385
  deps.store.getDocumentsByDocids(docids, { activeOnly: true }),
380
386
  deps.store.getContentBatch(mirrorHashes),
381
387
  deps.store.getChunksBatch(mirrorHashes),
382
388
  deps.resolveCurrentRanks?.(structuredClone(capsule)).catch(() => null) ??
383
389
  Promise.resolve(null),
390
+ deps.store.getSupersedingUris?.([...evidenceUris]) ??
391
+ Promise.resolve(ok(new Map<string, string>())),
384
392
  ]);
385
393
  const loaded: LoadedEvidence = {
386
394
  snapshot: before,
395
+ successors: unwrapStore(
396
+ successors,
397
+ "document_load_failed",
398
+ "Failed to load supersession of verification evidence"
399
+ ),
387
400
  documents: unwrapStore(
388
401
  documentResult,
389
402
  "document_load_failed",