@gmickel/gno 2.3.2 → 2.4.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 (88) hide show
  1. package/README.md +24 -4
  2. package/assets/skill/SKILL.md +30 -0
  3. package/assets/skill/cli-reference.md +12 -0
  4. package/assets/skill/examples.md +15 -0
  5. package/assets/skill/mcp-reference.md +16 -0
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.3.2.zip → gno-browser-clipper-v2.4.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/spec/cli.md +30 -0
  12. package/spec/db/schema.sql +2 -0
  13. package/spec/evals.md +48 -0
  14. package/spec/mcp.md +12 -0
  15. package/spec/output-schemas/ask.schema.json +12 -0
  16. package/spec/output-schemas/context-capsule-v1.schema.json +205 -32
  17. package/spec/output-schemas/context-capsule-verification.schema.json +32 -9
  18. package/spec/output-schemas/evidence-fixtures.schema.json +176 -0
  19. package/spec/output-schemas/evidence-report.schema.json +322 -0
  20. package/spec/output-schemas/evidence-run.schema.json +277 -0
  21. package/spec/output-schemas/metadata-predicate.schema.json +460 -0
  22. package/spec/output-schemas/retrieval-trace-filters.schema.json +13 -4
  23. package/spec/output-schemas/search-results.schema.json +12 -0
  24. package/spec/output-schemas/status.schema.json +37 -6
  25. package/src/app/context-runtime-contract.ts +33 -2
  26. package/src/app/context-runtime-input.ts +17 -0
  27. package/src/app/context-runtime-types.ts +2 -0
  28. package/src/app/context-runtime.ts +27 -3
  29. package/src/app/context-surface.ts +2 -0
  30. package/src/app/verified-ask.ts +1 -0
  31. package/src/cli/commands/ask.ts +1 -0
  32. package/src/cli/commands/context-build.ts +1 -0
  33. package/src/cli/commands/search.ts +1 -0
  34. package/src/cli/commands/status.ts +10 -0
  35. package/src/cli/options.ts +19 -0
  36. package/src/cli/program.ts +12 -0
  37. package/src/converters/types.ts +2 -0
  38. package/src/core/context-capsule-retrieval-schema.ts +8 -0
  39. package/src/core/context-capsule-schema.ts +47 -3
  40. package/src/core/context-capsule-validation.ts +14 -2
  41. package/src/core/context-capsule-verification.ts +1 -0
  42. package/src/core/context-capsule.ts +16 -2
  43. package/src/core/context-compiler.ts +3 -0
  44. package/src/core/record-metadata.ts +1 -1
  45. package/src/core/retrieval-replay-candidate.ts +5 -0
  46. package/src/core/retrieval-trace-filter-normalization.ts +4 -0
  47. package/src/core/retrieval-trace-request.ts +1 -0
  48. package/src/core/retrieval-trace-session.ts +5 -1
  49. package/src/core/retrieval-trace.ts +2 -0
  50. package/src/core/typed-metadata.ts +224 -0
  51. package/src/ingestion/record-adapter-canonical.ts +13 -2
  52. package/src/ingestion/record-container.ts +9 -0
  53. package/src/ingestion/sync.ts +11 -1
  54. package/src/ingestion/typed-metadata.ts +55 -0
  55. package/src/mcp/tools/ask.ts +10 -2
  56. package/src/mcp/tools/context.ts +1 -0
  57. package/src/mcp/tools/index.ts +12 -1
  58. package/src/mcp/tools/query.ts +13 -0
  59. package/src/mcp/tools/search.ts +9 -0
  60. package/src/mcp/tools/status.ts +4 -0
  61. package/src/mcp/tools/vsearch.ts +9 -0
  62. package/src/pipeline/diagnose.ts +6 -0
  63. package/src/pipeline/filters.ts +65 -0
  64. package/src/pipeline/graph-retrieval.ts +9 -1
  65. package/src/pipeline/hybrid.ts +22 -2
  66. package/src/pipeline/search.ts +11 -0
  67. package/src/pipeline/types.ts +7 -1
  68. package/src/pipeline/vsearch.ts +21 -1
  69. package/src/sdk/client.ts +29 -1
  70. package/src/sdk/index.ts +4 -0
  71. package/src/sdk/types.ts +7 -0
  72. package/src/serve/context-capsule.ts +1 -0
  73. package/src/serve/public/app.tsx +1 -1
  74. package/src/serve/public/components/MetadataFilter.tsx +186 -0
  75. package/src/serve/public/globals.built.css +1 -1
  76. package/src/serve/public/lib/metadata-filter.ts +28 -0
  77. package/src/serve/public/lib/retrieval-filters.ts +3 -0
  78. package/src/serve/public/pages/Ask.tsx +48 -3
  79. package/src/serve/public/pages/DocView.tsx +33 -0
  80. package/src/serve/public/pages/Search.tsx +44 -1
  81. package/src/serve/routes/api.ts +50 -0
  82. package/src/store/migrations/030-typed-metadata.ts +13 -0
  83. package/src/store/migrations/index.ts +2 -0
  84. package/src/store/sqlite/adapter.ts +55 -4
  85. package/src/store/sqlite/eligibility.ts +12 -0
  86. package/src/store/sqlite/metadata-predicate.ts +71 -0
  87. package/src/store/types.ts +11 -0
  88. package/browser-extension/artifacts/gno-browser-clipper-v2.3.2.zip.sha256 +0 -1
@@ -63,6 +63,7 @@ export const buildContextCapsule = async (
63
63
  deps.config.collections.map((collection) => collection.name)
64
64
  );
65
65
  const noRerank = normalized.depthPolicy === "fast" || normalized.noRerank;
66
+ let metadataCoverageIncomplete = false;
66
67
  const plan = await compileContextEvidence<ContextCapsuleV1>(
67
68
  {
68
69
  goal: normalized.goal,
@@ -74,6 +75,7 @@ export const buildContextCapsule = async (
74
75
  tagsAll: normalized.tagsAll,
75
76
  tagsAny: normalized.tagsAny,
76
77
  categories: normalized.categories,
78
+ filter: normalized.filter,
77
79
  author: normalized.author ?? undefined,
78
80
  lang: normalized.lang ?? undefined,
79
81
  intent: normalized.intent ?? undefined,
@@ -104,8 +106,14 @@ export const buildContextCapsule = async (
104
106
  store: deps.store,
105
107
  hydration: deps.hydration,
106
108
  config: deps.config,
107
- vectorIndex: deps.vectorIndex ?? null,
108
- embedPort: deps.embedPort ?? null,
109
+ vectorIndex:
110
+ normalized.depthPolicy === "fast"
111
+ ? null
112
+ : (deps.vectorIndex ?? null),
113
+ embedPort:
114
+ normalized.depthPolicy === "fast"
115
+ ? null
116
+ : (deps.embedPort ?? null),
109
117
  expandPort: null,
110
118
  rerankPort: requestNoRerank ? null : (deps.rerankPort ?? null),
111
119
  },
@@ -124,10 +132,26 @@ export const buildContextCapsule = async (
124
132
  result.error.cause
125
133
  );
126
134
  }
135
+ if (
136
+ normalized.filter &&
137
+ result.value.meta.warnings?.some(
138
+ (warning) =>
139
+ warning.code === "METADATA_COVERAGE_UNKNOWN" ||
140
+ warning.code === "METADATA_COVERAGE_INCOMPLETE"
141
+ )
142
+ ) {
143
+ metadataCoverageIncomplete = true;
144
+ }
127
145
  return result.value;
128
146
  },
129
147
  projectCanonical: (draft, snapshots) =>
130
- projectContextCapsule(draft, snapshots, normalized, deps),
148
+ projectContextCapsule(
149
+ draft,
150
+ snapshots,
151
+ normalized,
152
+ deps,
153
+ metadataCoverageIncomplete
154
+ ),
131
155
  }
132
156
  );
133
157
  if (!plan.projection) {
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import type { ContextCapsuleBuildInput } from "./context-runtime-types";
6
6
 
7
7
  import { ContextCapsuleContractError } from "../core/context-capsule";
8
+ import { metadataPredicateSchema } from "../core/typed-metadata";
8
9
 
9
10
  const queryModeSchema = z
10
11
  .object({
@@ -28,6 +29,7 @@ export const contextBuildSurfaceSchema = z
28
29
  tagsAll: stringList.optional(),
29
30
  tagsAny: stringList.optional(),
30
31
  categories: stringList.optional(),
32
+ filter: metadataPredicateSchema.optional(),
31
33
  author: z.string().optional(),
32
34
  lang: z.string().optional(),
33
35
  intent: z.string().optional(),
@@ -184,6 +184,7 @@ export const buildVerifiedAsk = async (
184
184
  tagsAll: options.tagsAll,
185
185
  tagsAny: options.tagsAny,
186
186
  categories: options.categories,
187
+ filter: options.filter,
187
188
  author: options.author,
188
189
  lang: options.lang,
189
190
  intent: options.intent,
@@ -323,6 +323,7 @@ export async function ask(
323
323
  until: options.until,
324
324
  categories: options.categories,
325
325
  author: options.author,
326
+ filter: options.filter,
326
327
  tagsAll: options.tagsAll,
327
328
  tagsAny: options.tagsAny,
328
329
  exclude: options.exclude,
@@ -124,6 +124,7 @@ export const contextBuild = async (
124
124
  until: options.until,
125
125
  categories: options.categories,
126
126
  author: options.author,
127
+ filter: options.filter,
127
128
  graph: options.graph,
128
129
  candidateLimit: options.candidateLimit,
129
130
  queryModes: options.queryModes,
@@ -113,6 +113,7 @@ export async function search(
113
113
  until: options.until,
114
114
  categories: options.categories,
115
115
  author: options.author,
116
+ filter: options.filter,
116
117
  intent: options.intent,
117
118
  exclude: options.exclude,
118
119
  },
@@ -87,6 +87,11 @@ function formatTerminal(
87
87
  ): string {
88
88
  const lines: string[] = [];
89
89
 
90
+ if (indexStatus.typedMetadata) {
91
+ lines.push(
92
+ `Typed metadata: ${indexStatus.typedMetadata.pending} pending sync, ${indexStatus.typedMetadata.invalid} invalid`
93
+ );
94
+ }
90
95
  lines.push(`Index: ${indexStatus.indexName}`);
91
96
  lines.push(`Config: ${indexStatus.configPath}`);
92
97
  lines.push(`Database: ${indexStatus.dbPath}`);
@@ -200,6 +205,10 @@ function formatMarkdown(
200
205
  lines.push(`- **Documents**: ${indexStatus.activeDocuments}`);
201
206
  lines.push(`- **Chunks**: ${indexStatus.totalChunks}`);
202
207
  lines.push(`- **Embedding backlog**: ${indexStatus.embeddingBacklog}`);
208
+ if (indexStatus.typedMetadata)
209
+ lines.push(
210
+ `- **Typed metadata**: ${indexStatus.typedMetadata.pending} pending sync, ${indexStatus.typedMetadata.invalid} invalid`
211
+ );
203
212
  lines.push(`- **Recent errors**: ${indexStatus.recentErrors}`);
204
213
 
205
214
  const chunking = formatChunkingStatus(indexStatus.chunking);
@@ -347,6 +356,7 @@ export function formatStatus(
347
356
  totalDocuments: s.activeDocuments,
348
357
  totalChunks: s.totalChunks,
349
358
  embeddingBacklog: s.embeddingBacklog,
359
+ typedMetadata: s.typedMetadata,
350
360
  chunking: s.chunking,
351
361
  lastUpdated: s.lastUpdatedAt,
352
362
  healthy: isStatusHealthy(s, result.activation),
@@ -9,6 +9,10 @@ import {
9
9
  normalizeProjectAffinityValues,
10
10
  ProjectAffinityInputError,
11
11
  } from "../core/project-affinity-surface";
12
+ import {
13
+ normalizeMetadataPredicate,
14
+ type MetadataPredicate,
15
+ } from "../core/typed-metadata";
12
16
  import { CliError } from "./errors";
13
17
 
14
18
  // ─────────────────────────────────────────────────────────────────────────────
@@ -259,3 +263,18 @@ export function parseOptionalFloat(
259
263
  }
260
264
  return num;
261
265
  }
266
+
267
+ export const parseCliMetadataFilter = (
268
+ value: unknown
269
+ ): MetadataPredicate | undefined => {
270
+ if (value === undefined) return undefined;
271
+ try {
272
+ if (typeof value !== "string") throw new Error("Expected JSON text");
273
+ return normalizeMetadataPredicate(JSON.parse(value));
274
+ } catch (error) {
275
+ throw new CliError(
276
+ "VALIDATION",
277
+ `filter: ${error instanceof Error ? error.message : "Invalid predicate"}`
278
+ );
279
+ }
280
+ };
@@ -49,6 +49,7 @@ import {
49
49
  collectRepeatableValue,
50
50
  getDefaultLimit,
51
51
  parseCliProjectAffinityOptions,
52
+ parseCliMetadataFilter,
52
53
  parseOptionalFloat,
53
54
  parsePositiveInt,
54
55
  } from "./options";
@@ -652,6 +653,7 @@ function wireSearchCommands(program: Command): void {
652
653
  "modified-at upper bound (ISO date/time or token)"
653
654
  )
654
655
  .option("--category <values>", "require category match (comma-separated)")
656
+ .option("--filter <json>", "typed custom metadata predicate (JSON)")
655
657
  .option("--author <text>", "filter by author (case-insensitive contains)")
656
658
  .option("--intent <text>", "disambiguating context for ambiguous queries")
657
659
  .option(
@@ -736,6 +738,7 @@ function wireSearchCommands(program: Command): void {
736
738
  until: cmdOpts.until as string | undefined,
737
739
  categories,
738
740
  author: cmdOpts.author as string | undefined,
741
+ filter: parseCliMetadataFilter(cmdOpts.filter),
739
742
  intent: cmdOpts.intent as string | undefined,
740
743
  exclude,
741
744
  tagsAll,
@@ -790,6 +793,7 @@ function wireSearchCommands(program: Command): void {
790
793
  "modified-at upper bound (ISO date/time or token)"
791
794
  )
792
795
  .option("--category <values>", "require category match (comma-separated)")
796
+ .option("--filter <json>", "typed custom metadata predicate (JSON)")
793
797
  .option("--author <text>", "filter by author (case-insensitive contains)")
794
798
  .option("--intent <text>", "disambiguating context for ambiguous queries")
795
799
  .option(
@@ -864,6 +868,7 @@ function wireSearchCommands(program: Command): void {
864
868
  until: cmdOpts.until as string | undefined,
865
869
  categories,
866
870
  author: cmdOpts.author as string | undefined,
871
+ filter: parseCliMetadataFilter(cmdOpts.filter),
867
872
  intent: cmdOpts.intent as string | undefined,
868
873
  exclude,
869
874
  tagsAll,
@@ -916,6 +921,7 @@ function wireSearchCommands(program: Command): void {
916
921
  "modified-at upper bound (ISO date/time or token)"
917
922
  )
918
923
  .option("--category <values>", "require category match (comma-separated)")
924
+ .option("--filter <json>", "typed custom metadata predicate (JSON)")
919
925
  .option("--author <text>", "filter by author (case-insensitive contains)")
920
926
  .option("--intent <text>", "disambiguating context for ambiguous queries")
921
927
  .option(
@@ -1080,6 +1086,7 @@ function wireSearchCommands(program: Command): void {
1080
1086
  until: cmdOpts.until as string | undefined,
1081
1087
  categories,
1082
1088
  author: cmdOpts.author as string | undefined,
1089
+ filter: parseCliMetadataFilter(cmdOpts.filter),
1083
1090
  intent: cmdOpts.intent as string | undefined,
1084
1091
  exclude,
1085
1092
  tagsAll,
@@ -1116,6 +1123,7 @@ function wireSearchCommands(program: Command): void {
1116
1123
  until: cmdOpts.until as string | undefined,
1117
1124
  categories,
1118
1125
  author: cmdOpts.author as string | undefined,
1126
+ filter: parseCliMetadataFilter(cmdOpts.filter),
1119
1127
  intent: cmdOpts.intent as string | undefined,
1120
1128
  exclude,
1121
1129
  tagsAll,
@@ -1219,6 +1227,7 @@ function wireSearchCommands(program: Command): void {
1219
1227
  "modified-at upper bound (ISO date/time or token)"
1220
1228
  )
1221
1229
  .option("--category <values>", "require category match (comma-separated)")
1230
+ .option("--filter <json>", "typed custom metadata predicate (JSON)")
1222
1231
  .option("--author <text>", "filter by author (case-insensitive contains)")
1223
1232
  .option("--intent <text>", "disambiguating context for ambiguous queries")
1224
1233
  .option(
@@ -1350,6 +1359,7 @@ function wireSearchCommands(program: Command): void {
1350
1359
  until: cmdOpts.until as string | undefined,
1351
1360
  categories,
1352
1361
  author: cmdOpts.author as string | undefined,
1362
+ filter: parseCliMetadataFilter(cmdOpts.filter),
1353
1363
  intent: cmdOpts.intent as string | undefined,
1354
1364
  exclude,
1355
1365
  minScore,
@@ -2695,6 +2705,7 @@ function wireManagementCommands(program: Command): void {
2695
2705
  .option("--tags-all <tags>", "require ALL tags (comma-separated)")
2696
2706
  .option("--tags-any <tags>", "require ANY tag (comma-separated)")
2697
2707
  .option("--category <values>", "require category match (comma-separated)")
2708
+ .option("--filter <json>", "typed custom metadata predicate (JSON)")
2698
2709
  .option("--author <text>", "filter by author")
2699
2710
  .option("--lang <code>", "language hint (BCP-47)")
2700
2711
  .option("--since <date>", "modified-at lower bound")
@@ -2765,6 +2776,7 @@ function wireManagementCommands(program: Command): void {
2765
2776
  tagsAny: parseCsvValues(cmdOpts.tagsAny),
2766
2777
  categories: parseCsvValues(cmdOpts.category),
2767
2778
  author: cmdOpts.author as string | undefined,
2779
+ filter: parseCliMetadataFilter(cmdOpts.filter),
2768
2780
  lang: cmdOpts.lang as string | undefined,
2769
2781
  since: cmdOpts.since as string | undefined,
2770
2782
  until: cmdOpts.until as string | undefined,
@@ -1,3 +1,4 @@
1
+ import type { TypedMetadata } from "../core/typed-metadata";
1
2
  /**
2
3
  * Converter subsystem types.
3
4
  * PRD §8.2 - Converter interfaces
@@ -157,6 +158,7 @@ export const RECORD_METADATA_LIMITS = {
157
158
 
158
159
  /** Metadata shared by export adapters and later search/get projections. */
159
160
  export interface RecordMetadata {
161
+ custom?: TypedMetadata;
160
162
  author?: string;
161
163
  participants?: string[];
162
164
  categories?: string[];
@@ -3,6 +3,7 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { contextCapsuleIndexSnapshotSchema } from "./context-capsule-index-schema";
6
+ import { metadataPredicateSchema } from "./typed-metadata";
6
7
 
7
8
  const nonEmptyText = z.string().min(1);
8
9
  const capabilityOutcomeSchema = z.enum([
@@ -83,3 +84,10 @@ export const contextCapsuleRetrievalSchema = z
83
84
  export type ContextCapabilityState = z.infer<
84
85
  typeof contextCapabilityStateSchema
85
86
  >;
87
+
88
+ export const contextCapsuleRetrievalV1_2Schema =
89
+ contextCapsuleRetrievalSchema.extend({
90
+ request: contextCapsuleRetrievalSchema.shape.request.extend({
91
+ filter: metadataPredicateSchema,
92
+ }),
93
+ });
@@ -4,6 +4,7 @@ import { buildUri, deriveDocid, parseUri } from "../app/constants";
4
4
  import { isValidIndexName } from "../app/index-name";
5
5
  import { RECORD_METADATA_LIMITS } from "../converters/types";
6
6
  import { contextCapsuleIndexSnapshotSchema } from "./context-capsule-index-schema";
7
+ import { contextCapsuleRetrievalV1_2Schema } from "./context-capsule-retrieval-schema";
7
8
  import { contextCapsuleRetrievalSchema } from "./context-capsule-retrieval-schema";
8
9
  import {
9
10
  contextCapsuleEvidenceIdentity,
@@ -12,11 +13,15 @@ import {
12
13
  validateContextCapsulePayload,
13
14
  } from "./context-capsule-validation";
14
15
  import { egressLineageSchema } from "./egress-provenance";
16
+ import {
17
+ normalizeMetadataPredicate,
18
+ metadataPredicateSchema,
19
+ } from "./typed-metadata";
15
20
 
16
21
  export { contextCapsuleIndexSnapshotSchema } from "./context-capsule-index-schema";
17
22
 
18
23
  export const CONTEXT_CAPSULE_SCHEMA_VERSION = "1.0" as const;
19
- export const CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION = "1.1" as const;
24
+ export const CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION = "1.2" as const;
20
25
  export const CONTEXT_CAPSULE_COORDINATE_SPACE = "canonical_mirror" as const;
21
26
 
22
27
  const SHA256_PATTERN = /^[a-f0-9]{64}$/;
@@ -654,7 +659,7 @@ const validateContextCapsuleV1_1Lineage = (
654
659
 
655
660
  export const contextCapsulePayloadV1_1Schema = z
656
661
  .object({
657
- schemaVersion: z.literal(CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION),
662
+ schemaVersion: z.literal("1.1"),
658
663
  coordinateSpace: z.literal(CONTEXT_CAPSULE_COORDINATE_SPACE),
659
664
  goal: nonEmptyTextSchema,
660
665
  query: nonEmptyTextSchema,
@@ -682,6 +687,45 @@ export type ContextCapsulePayloadV1_1 = z.infer<
682
687
  typeof contextCapsulePayloadV1_1Schema
683
688
  >;
684
689
 
690
+ export const contextCapsulePayloadV1_2Schema = z
691
+ .object({
692
+ ...contextCapsulePayloadV1_1Schema.shape,
693
+ schemaVersion: z.literal(CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION),
694
+ scope: scopeSchema.extend({ filter: metadataPredicateSchema }),
695
+ retrieval: contextCapsuleRetrievalV1_2Schema,
696
+ warnings: z
697
+ .array(
698
+ z
699
+ .object({
700
+ code: z.union([
701
+ contextCapsuleWarningCodeSchema,
702
+ z.literal("metadata_coverage_incomplete"),
703
+ ]),
704
+ })
705
+ .strict()
706
+ )
707
+ .max(32),
708
+ })
709
+ .strict()
710
+ .superRefine((value, context) => {
711
+ validateContextCapsulePayload(value, context);
712
+ validateContextCapsuleV1_1Lineage(value, context);
713
+ if (
714
+ JSON.stringify(normalizeMetadataPredicate(value.scope.filter)) !==
715
+ JSON.stringify(normalizeMetadataPredicate(value.retrieval.request.filter))
716
+ ) {
717
+ context.addIssue({
718
+ code: "custom",
719
+ path: ["retrieval", "request", "filter"],
720
+ message: "Retrieval predicate must match scope predicate",
721
+ });
722
+ }
723
+ });
724
+ export type ContextCapsulePayloadV1_2 = z.infer<
725
+ typeof contextCapsulePayloadV1_2Schema
726
+ >;
727
+
685
728
  export type ContextCapsulePayload =
686
729
  | ContextCapsulePayloadV1
687
- | ContextCapsulePayloadV1_1;
730
+ | ContextCapsulePayloadV1_1
731
+ | ContextCapsulePayloadV1_2;
@@ -318,11 +318,18 @@ const validateCoverageBindings = (
318
318
  path: ["coverage", "gaps"],
319
319
  });
320
320
  }
321
- const complete = unresolved.length === 0 && value.coverage.gaps.length === 0;
321
+ const metadataIncomplete =
322
+ value.schemaVersion === "1.2" &&
323
+ value.warnings.some(({ code }) => code === "metadata_coverage_incomplete");
324
+ const complete =
325
+ unresolved.length === 0 &&
326
+ value.coverage.gaps.length === 0 &&
327
+ !metadataIncomplete;
322
328
  if (value.coverage.complete !== complete) {
323
329
  context.addIssue({
324
330
  code: "custom",
325
- message: "complete must reflect unresolved facets and gaps",
331
+ message:
332
+ "complete must reflect unresolved facets, gaps, and metadata coverage",
326
333
  path: ["coverage", "complete"],
327
334
  });
328
335
  }
@@ -379,6 +386,11 @@ export const validateContextCapsulePayload = (
379
386
  });
380
387
  }
381
388
  const expectedWarnings = new Set<string>();
389
+ if (
390
+ value.schemaVersion === "1.2" &&
391
+ warningSet.has("metadata_coverage_incomplete")
392
+ )
393
+ expectedWarnings.add("metadata_coverage_incomplete");
382
394
  if (!value.coverage.complete) expectedWarnings.add("incomplete_coverage");
383
395
  if (value.omissions.truncated) expectedWarnings.add("omissions_truncated");
384
396
  if (fallbackTokenizer) expectedWarnings.add("token_estimate_used");
@@ -123,6 +123,7 @@ export const contextCapsuleVerificationSchema = z
123
123
  .object({
124
124
  schemaVersion: z.union([
125
125
  z.literal(CONTEXT_CAPSULE_SCHEMA_VERSION),
126
+ z.literal("1.1"),
126
127
  z.literal(CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION),
127
128
  ]),
128
129
  coordinateSpace: z.literal(CONTEXT_CAPSULE_COORDINATE_SPACE),
@@ -4,9 +4,11 @@ import { canonicalizeIndexName } from "../app/index-name";
4
4
  import {
5
5
  contextCapsulePayloadV1Schema,
6
6
  contextCapsulePayloadV1_1Schema,
7
+ contextCapsulePayloadV1_2Schema,
7
8
  type ContextCapsulePayload,
8
9
  } from "./context-capsule-schema";
9
10
  import { createEgressLineage, type EgressLineage } from "./egress-provenance";
11
+ import { normalizeMetadataPredicate } from "./typed-metadata";
10
12
 
11
13
  const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
12
14
 
@@ -61,6 +63,9 @@ const normalizePayload = (
61
63
  query: normalizeText(value.query),
62
64
  scope: {
63
65
  ...value.scope,
66
+ ...(value.schemaVersion === "1.2"
67
+ ? { filter: normalizeMetadataPredicate(value.scope.filter) }
68
+ : {}),
64
69
  indexName: canonicalizeIndexName(value.scope.indexName),
65
70
  collections: normalizeSet(value.scope.collections),
66
71
  uriPrefix:
@@ -79,6 +84,13 @@ const normalizePayload = (
79
84
  queryVariants: value.retrieval.queryVariants.map(normalizeText),
80
85
  request: {
81
86
  ...value.retrieval.request,
87
+ ...(value.schemaVersion === "1.2"
88
+ ? {
89
+ filter: normalizeMetadataPredicate(
90
+ value.retrieval.request.filter
91
+ ),
92
+ }
93
+ : {}),
82
94
  author:
83
95
  value.retrieval.request.author === null
84
96
  ? null
@@ -116,7 +128,7 @@ const normalizePayload = (
116
128
  `${right.code}\0${right.capability}`
117
129
  )
118
130
  ),
119
- ...(value.schemaVersion === "1.1"
131
+ ...(value.schemaVersion !== "1.0"
120
132
  ? {
121
133
  egressLineage: {
122
134
  ...value.egressLineage,
@@ -291,6 +303,7 @@ const contractError = (
291
303
  const contextCapsulePayloadSchema = z.union([
292
304
  contextCapsulePayloadV1Schema,
293
305
  contextCapsulePayloadV1_1Schema,
306
+ contextCapsulePayloadV1_2Schema,
294
307
  ]);
295
308
 
296
309
  const parsePayload = (input: unknown): ContextCapsulePayload => {
@@ -391,6 +404,7 @@ const fixedPointCapsule = (
391
404
  const contextCapsuleBaseV1Schema = z.union([
392
405
  contextCapsulePayloadV1Schema.extend({ capsuleId: sha256Schema }),
393
406
  contextCapsulePayloadV1_1Schema.extend({ capsuleId: sha256Schema }),
407
+ contextCapsulePayloadV1_2Schema.extend({ capsuleId: sha256Schema }),
394
408
  ]);
395
409
 
396
410
  export const contextCapsuleV1Schema = contextCapsuleBaseV1Schema.superRefine(
@@ -503,7 +517,7 @@ export const canonicalContextCapsuleAccountingJson = (input: unknown): string =>
503
517
  */
504
518
  export const contextCapsuleEgressLineage = (input: unknown): EgressLineage => {
505
519
  const capsule = parseContextCapsuleV1(input);
506
- if (capsule.schemaVersion === "1.1") return capsule.egressLineage;
520
+ if (capsule.schemaVersion !== "1.0") return capsule.egressLineage;
507
521
  return createEgressLineage(
508
522
  [...new Set(capsule.evidence.map(({ collection }) => collection))].map(
509
523
  (collection) => ({
@@ -26,6 +26,7 @@ import type {
26
26
  MaterializedContextCandidate,
27
27
  } from "./context-budget";
28
28
  import type { ContextConfiguredGuidance } from "./context-guidance";
29
+ import type { MetadataPredicate } from "./typed-metadata";
29
30
 
30
31
  import { decorateUriForIndex } from "../app/constants";
31
32
  import { canonicalizeIndexName } from "../app/index-name";
@@ -96,6 +97,7 @@ export interface ContextCompilerInput {
96
97
  tagsAll?: string[];
97
98
  tagsAny?: string[];
98
99
  categories?: string[];
100
+ filter?: MetadataPredicate;
99
101
  author?: string;
100
102
  lang?: string;
101
103
  intent?: string;
@@ -358,6 +360,7 @@ export const planContextEvidence = async <T, P>(
358
360
  tagsAll: input.tagsAll,
359
361
  tagsAny: input.tagsAny,
360
362
  categories: input.categories,
363
+ filter: input.filter,
361
364
  author: input.author,
362
365
  lang: input.lang,
363
366
  intent: input.intent,
@@ -1,6 +1,6 @@
1
1
  import type { RecordAnchor, RecordMetadata } from "../converters/types";
2
2
 
3
- export interface RecordEvidenceMetadata extends RecordMetadata {
3
+ export interface RecordEvidenceMetadata extends Omit<RecordMetadata, "custom"> {
4
4
  recordKey: string;
5
5
  sourceLocator: string;
6
6
  anchors: RecordAnchor[];
@@ -18,6 +18,7 @@ import { searchBm25 } from "../pipeline/search";
18
18
  import { SEARCH_RESULTS_TRACE_METADATA } from "../pipeline/types";
19
19
  import { searchVector } from "../pipeline/vsearch";
20
20
  import { err, ok } from "../store/types";
21
+ import { normalizeMetadataPredicate } from "./typed-metadata";
21
22
 
22
23
  export interface RetrievalReplayDeps extends HybridSearchDeps {
23
24
  indexName?: string;
@@ -108,6 +109,10 @@ export const buildRetrievalReplaySearchOptions = (
108
109
  categories: Array.isArray(filters.categories)
109
110
  ? (filters.categories as string[])
110
111
  : undefined,
112
+ filter:
113
+ filters.filter === undefined
114
+ ? undefined
115
+ : normalizeMetadataPredicate(filters.filter),
111
116
  author: typeof filters.author === "string" ? filters.author : undefined,
112
117
  intent: typeof filters.intent === "string" ? filters.intent : undefined,
113
118
  exclude: Array.isArray(filters.exclude)
@@ -1,5 +1,7 @@
1
1
  /** Deterministic set-like retrieval-filter normalization before persistence. */
2
2
 
3
+ import { normalizeMetadataPredicate } from "./typed-metadata";
4
+
3
5
  const SET_ARRAY_KEYS = [
4
6
  "categories",
5
7
  "collections",
@@ -23,5 +25,7 @@ export const canonicalizeRetrievalTraceFilters = <
23
25
  normalized[key] = [...new Set(values)].sort(compareCodeUnits);
24
26
  }
25
27
  }
28
+ if (filters.filter !== undefined)
29
+ normalized.filter = normalizeMetadataPredicate(filters.filter);
26
30
  return normalized as Filters;
27
31
  };
@@ -122,6 +122,7 @@ export const retrievalTraceFilters = (
122
122
  since: options.since,
123
123
  until: options.until,
124
124
  categories: options.categories,
125
+ filter: options.filter,
125
126
  author: options.author,
126
127
  intent: options.intent,
127
128
  exclude: options.exclude,
@@ -380,7 +380,11 @@ export class RetrievalTraceSession {
380
380
  latencyMs?: number
381
381
  ): Promise<StoreResult<"inserted" | "duplicate" | "disabled">> {
382
382
  if (!this.hasCapacity(2)) return ok("disabled");
383
- if (capsule.schemaVersion === "1.0" || capsule.schemaVersion === "1.1") {
383
+ if (
384
+ capsule.schemaVersion === "1.0" ||
385
+ capsule.schemaVersion === "1.1" ||
386
+ capsule.schemaVersion === "1.2"
387
+ ) {
384
388
  const widened = await this.ensureLineageCoverage([
385
389
  contextCapsuleEgressLineage(capsule),
386
390
  ]);
@@ -29,6 +29,7 @@ import {
29
29
  legacyLocalOnlyEgressLineage,
30
30
  } from "./egress-provenance";
31
31
  import { canonicalizeRetrievalTraceFilters } from "./retrieval-trace-filter-normalization";
32
+ import { metadataPredicateSchema } from "./typed-metadata";
32
33
 
33
34
  const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
34
35
  const gnoUriSchema = z
@@ -57,6 +58,7 @@ export const traceFiltersSchema = z
57
58
  since: z.string().max(128).optional(),
58
59
  until: z.string().max(128).optional(),
59
60
  categories: z.array(z.string().max(512)).max(1000).optional(),
61
+ filter: metadataPredicateSchema.optional(),
60
62
  author: z.string().max(1024).optional(),
61
63
  intent: z.string().max(8192).optional(),
62
64
  exclude: z.array(z.string().max(1024)).max(1000).optional(),