@gmickel/gno 1.18.0 → 1.20.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 (153) hide show
  1. package/README.md +14 -7
  2. package/assets/skill/SKILL.md +54 -12
  3. package/assets/skill/mcp-reference.md +7 -2
  4. package/assets/skill/recipes/citation-and-provenance.md +32 -9
  5. package/package.json +2 -1
  6. package/spec/AGENTS.md +83 -0
  7. package/spec/CLAUDE.md +83 -0
  8. package/spec/bench-fixture.schema.json +137 -0
  9. package/spec/cli.md +2919 -0
  10. package/spec/db/schema.sql +442 -0
  11. package/spec/evals-agentic.md +592 -0
  12. package/spec/evals.md +1106 -0
  13. package/spec/mcp.md +2279 -0
  14. package/spec/output-schemas/activation-verification.schema.json +515 -0
  15. package/spec/output-schemas/ask.schema.json +564 -0
  16. package/spec/output-schemas/backlinks.schema.json +131 -0
  17. package/spec/output-schemas/bench-result.schema.json +120 -0
  18. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  19. package/spec/output-schemas/claim-verification.schema.json +291 -0
  20. package/spec/output-schemas/collection-list.schema.json +45 -0
  21. package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
  22. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  23. package/spec/output-schemas/context-list.schema.json +21 -0
  24. package/spec/output-schemas/doctor.schema.json +313 -0
  25. package/spec/output-schemas/error.schema.json +30 -0
  26. package/spec/output-schemas/expansion.schema.json +37 -0
  27. package/spec/output-schemas/get.schema.json +140 -0
  28. package/spec/output-schemas/graph-query.schema.json +99 -0
  29. package/spec/output-schemas/graph.schema.json +371 -0
  30. package/spec/output-schemas/links-list.schema.json +186 -0
  31. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  32. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  33. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  34. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  35. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  36. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  37. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  38. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  39. package/spec/output-schemas/models-list.schema.json +93 -0
  40. package/spec/output-schemas/multi-get.schema.json +103 -0
  41. package/spec/output-schemas/process-status.schema.json +119 -0
  42. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  43. package/spec/output-schemas/resident-status.schema.json +154 -0
  44. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  45. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  46. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  47. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  48. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  49. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  50. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  51. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  52. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  53. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  54. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  55. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  56. package/spec/output-schemas/search-result.schema.json +154 -0
  57. package/spec/output-schemas/search-results.schema.json +338 -0
  58. package/spec/output-schemas/similar.schema.json +84 -0
  59. package/spec/output-schemas/status.schema.json +676 -0
  60. package/spec/output-schemas/tags-list.schema.json +48 -0
  61. package/src/app/context-runtime-contract.ts +10 -5
  62. package/src/app/context-runtime-input.ts +29 -1
  63. package/src/app/context-runtime-types.ts +7 -0
  64. package/src/app/context-runtime.ts +20 -2
  65. package/src/app/context-surface.ts +4 -0
  66. package/src/app/verified-ask.ts +291 -0
  67. package/src/cli/commands/ask-format.ts +255 -0
  68. package/src/cli/commands/ask.ts +144 -183
  69. package/src/cli/commands/context-build.ts +56 -9
  70. package/src/cli/commands/get.ts +64 -3
  71. package/src/cli/commands/query.ts +62 -23
  72. package/src/cli/commands/replay.ts +140 -0
  73. package/src/cli/commands/search.ts +48 -3
  74. package/src/cli/commands/shared.ts +3 -1
  75. package/src/cli/commands/trace.ts +200 -0
  76. package/src/cli/commands/vsearch.ts +75 -53
  77. package/src/cli/program.ts +287 -1
  78. package/src/config/index.ts +9 -0
  79. package/src/config/retrieval-traces.ts +56 -0
  80. package/src/config/types.ts +4 -0
  81. package/src/core/context-budget.ts +6 -0
  82. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  83. package/src/core/context-capsule-schema.ts +17 -0
  84. package/src/core/context-capsule-validation.ts +3 -2
  85. package/src/core/context-capsule.ts +18 -0
  86. package/src/core/context-compiler.ts +44 -25
  87. package/src/core/context-evidence.ts +6 -0
  88. package/src/core/retrieval-qrels.ts +405 -0
  89. package/src/core/retrieval-replay-candidate.ts +368 -0
  90. package/src/core/retrieval-replay-types.ts +109 -0
  91. package/src/core/retrieval-replay-validation.ts +89 -0
  92. package/src/core/retrieval-replay.ts +441 -0
  93. package/src/core/retrieval-trace-evidence-origin.ts +178 -0
  94. package/src/core/retrieval-trace-export.ts +113 -0
  95. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  96. package/src/core/retrieval-trace-filters.ts +19 -0
  97. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  98. package/src/core/retrieval-trace-management-types.ts +132 -0
  99. package/src/core/retrieval-trace-management.ts +422 -0
  100. package/src/core/retrieval-trace-request.ts +141 -0
  101. package/src/core/retrieval-trace-session.ts +507 -0
  102. package/src/core/retrieval-trace.ts +472 -0
  103. package/src/llm/errors.ts +10 -1
  104. package/src/llm/httpGeneration.ts +11 -1
  105. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  106. package/src/llm/types.ts +6 -0
  107. package/src/mcp/tools/ask.ts +228 -0
  108. package/src/mcp/tools/context.ts +87 -15
  109. package/src/mcp/tools/get.ts +35 -1
  110. package/src/mcp/tools/index.ts +83 -0
  111. package/src/mcp/tools/query.ts +95 -64
  112. package/src/mcp/tools/search.ts +36 -13
  113. package/src/mcp/tools/trace.ts +143 -0
  114. package/src/mcp/tools/vsearch.ts +71 -38
  115. package/src/pipeline/answer.ts +167 -26
  116. package/src/pipeline/claim-verification-schema.ts +235 -0
  117. package/src/pipeline/claim-verification.ts +487 -0
  118. package/src/pipeline/claim-verifier.ts +474 -0
  119. package/src/pipeline/graph-retrieval.ts +15 -1
  120. package/src/pipeline/hybrid.ts +151 -43
  121. package/src/pipeline/search.ts +36 -3
  122. package/src/pipeline/trace-metadata.ts +47 -0
  123. package/src/pipeline/types.ts +68 -0
  124. package/src/pipeline/vsearch.ts +101 -38
  125. package/src/sdk/client.ts +415 -73
  126. package/src/sdk/documents.ts +48 -1
  127. package/src/sdk/index.ts +17 -0
  128. package/src/sdk/types.ts +28 -0
  129. package/src/serve/context-capsule.ts +67 -8
  130. package/src/serve/public/app.tsx +12 -1
  131. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  132. package/src/serve/public/globals.built.css +1 -1
  133. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  134. package/src/serve/public/pages/Ask.tsx +42 -4
  135. package/src/serve/public/pages/Dashboard.tsx +10 -0
  136. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  137. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  138. package/src/serve/retrieval-trace.ts +28 -0
  139. package/src/serve/routes/api.ts +508 -68
  140. package/src/serve/routes/traces.ts +156 -0
  141. package/src/serve/server.ts +87 -2
  142. package/src/store/index.ts +31 -0
  143. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  144. package/src/store/migrations/index.ts +2 -0
  145. package/src/store/retrieval-trace-codec.ts +384 -0
  146. package/src/store/sqlite/adapter.ts +153 -1
  147. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  148. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  149. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  150. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  151. package/src/store/types.ts +297 -0
  152. package/src/store/vector/sqlite-vec.ts +76 -1
  153. package/src/store/vector/types.ts +1 -1
@@ -5,6 +5,7 @@
5
5
  * @module src/pipeline/answer
6
6
  */
7
7
 
8
+ import type { RetrievalTraceSession } from "../core/retrieval-trace-session";
8
9
  import type { GenerationPort } from "../llm/types";
9
10
  import type { StorePort } from "../store/types";
10
11
  import type {
@@ -15,6 +16,11 @@ import type {
15
16
  } from "./types";
16
17
 
17
18
  import { buildAnswerPrompt, type AnswerPromptSource } from "./answer-prompt";
19
+ import { attachCitationTraceMetadata } from "./trace-metadata";
20
+ import {
21
+ CITATION_TRACE_METADATA,
22
+ SEARCH_RESULT_PLANNER_METADATA,
23
+ } from "./types";
18
24
 
19
25
  // ─────────────────────────────────────────────────────────────────────────────
20
26
  // Constants
@@ -24,6 +30,11 @@ import { buildAnswerPrompt, type AnswerPromptSource } from "./answer-prompt";
24
30
  export const ABSTENTION_MESSAGE =
25
31
  "I don't have enough information in the provided sources to answer this question.";
26
32
 
33
+ export const answerTraceTerminalStatus = (
34
+ citations: readonly Citation[] | undefined
35
+ ): "completed" | "partial" =>
36
+ citations && citations.length > 0 ? "completed" : "partial";
37
+
27
38
  /** Max characters per document (~8K tokens) */
28
39
  const MAX_DOC_CHARS = 32_000;
29
40
 
@@ -86,6 +97,65 @@ interface SelectedSource {
86
97
  reason: string;
87
98
  }
88
99
 
100
+ interface ExactAnswerPassage {
101
+ text: string;
102
+ startLine: number;
103
+ endLine: number;
104
+ passageHash: string;
105
+ }
106
+
107
+ const sha256 = (value: string): string =>
108
+ new Bun.CryptoHasher("sha256").update(value).digest("hex");
109
+
110
+ const completeLinePrefix = (
111
+ content: string,
112
+ startLine: number,
113
+ maxChars: number
114
+ ): ExactAnswerPassage | null => {
115
+ const lines = content.split("\n");
116
+ if (lines.length === 0) return null;
117
+ const selected: string[] = [];
118
+ let characters = 0;
119
+ for (const line of lines) {
120
+ const addition = selected.length === 0 ? line.length : line.length + 1;
121
+ if (characters + addition > maxChars) break;
122
+ selected.push(line);
123
+ characters += addition;
124
+ }
125
+ if (selected.length === 0) return null;
126
+ const text = selected.join("\n");
127
+ if (!text.trim()) return null;
128
+ return {
129
+ text,
130
+ startLine,
131
+ endLine: startLine + selected.length - 1,
132
+ passageHash: sha256(text),
133
+ };
134
+ };
135
+
136
+ const anchoredCompleteLines = (
137
+ content: string,
138
+ metadata: SearchResult[typeof SEARCH_RESULT_PLANNER_METADATA]
139
+ ): ExactAnswerPassage | null => {
140
+ if (
141
+ !metadata?.passageHash ||
142
+ metadata.startLine === undefined ||
143
+ metadata.endLine === undefined ||
144
+ metadata.endLine < metadata.startLine
145
+ ) {
146
+ return null;
147
+ }
148
+ const lines = content.split("\n");
149
+ const text = lines.slice(metadata.startLine - 1, metadata.endLine).join("\n");
150
+ if (
151
+ text.split("\n").length !== metadata.endLine - metadata.startLine + 1 ||
152
+ sha256(text) !== metadata.passageHash
153
+ ) {
154
+ return null;
155
+ }
156
+ return completeLinePrefix(text, metadata.startLine, MAX_DOC_CHARS);
157
+ };
158
+
89
159
  // ─────────────────────────────────────────────────────────────────────────────
90
160
  // Citation Processing
91
161
  // ─────────────────────────────────────────────────────────────────────────────
@@ -423,53 +493,78 @@ export async function generateGroundedAnswer(
423
493
  ): Promise<AnswerGenerationResult | null> {
424
494
  const { genPort, store } = deps;
425
495
  const sourceSelection = selectAdaptiveSources(query, results);
496
+ const finalRanks = new Map(
497
+ results.map((result, index) => [result, index + 1] as const)
498
+ );
426
499
  const promptSources: AnswerPromptSource[] = [];
427
500
  const citations: Citation[] = [];
428
501
  let citationIndex = 0;
429
502
 
430
503
  for (const r of sourceSelection.selected) {
431
- let content: string | null = null;
432
- let usedFullContent = false;
504
+ let passage: ExactAnswerPassage | null = null;
505
+ const plannerMetadata = r[SEARCH_RESULT_PLANNER_METADATA];
506
+ const sourceHash = r.source.sourceHash;
507
+ const mirrorHash = r.conversion?.mirrorHash;
433
508
 
434
509
  // Try to fetch full document content if store available
435
- if (store && r.conversion?.mirrorHash) {
436
- const contentResult = await store.getContent(r.conversion.mirrorHash);
510
+ if (store && mirrorHash) {
511
+ const contentResult = await store.getContent(mirrorHash);
437
512
  if (contentResult.ok && contentResult.value) {
438
- content = contentResult.value;
439
- usedFullContent = true;
440
- // Truncate to max doc chars
441
- if (content.length > MAX_DOC_CHARS) {
442
- content = `${content.slice(0, MAX_DOC_CHARS)}\n\n[... truncated ...]`;
513
+ if (sha256(contentResult.value) === mirrorHash) {
514
+ passage =
515
+ anchoredCompleteLines(contentResult.value, plannerMetadata) ??
516
+ completeLinePrefix(contentResult.value, 1, MAX_DOC_CHARS);
443
517
  }
444
518
  }
445
519
  }
446
520
 
447
- // Fallback to snippet if full content unavailable
448
- if (!content) {
449
- if (!r.snippet || r.snippet.trim().length === 0) {
450
- continue;
451
- }
452
- content =
453
- r.snippet.length > MAX_SNIPPET_CHARS
454
- ? `${r.snippet.slice(0, MAX_SNIPPET_CHARS)}...`
455
- : r.snippet;
521
+ // Snippet fallback is allowed only when the planner proves it is the exact
522
+ // complete canonical chunk, never an FTS marker/ellipsis presentation.
523
+ if (
524
+ !passage &&
525
+ plannerMetadata?.passageHash &&
526
+ plannerMetadata.startLine !== undefined &&
527
+ plannerMetadata.endLine !== undefined &&
528
+ sha256(r.snippet) === plannerMetadata.passageHash &&
529
+ r.snippet.split("\n").length ===
530
+ plannerMetadata.endLine - plannerMetadata.startLine + 1
531
+ ) {
532
+ passage = completeLinePrefix(
533
+ r.snippet,
534
+ plannerMetadata.startLine,
535
+ MAX_SNIPPET_CHARS
536
+ );
456
537
  }
538
+ if (!(passage && sourceHash && mirrorHash)) continue;
457
539
 
458
540
  citationIndex += 1;
459
541
  promptSources.push({
460
542
  index: citationIndex,
461
543
  docid: r.docid,
462
544
  uri: r.uri,
463
- content,
545
+ content: passage.text,
464
546
  guidance: r.context,
465
547
  });
466
- // Clear line range when citing full content (not a specific snippet)
467
- citations.push({
468
- docid: r.docid,
469
- uri: r.uri,
470
- startLine: usedFullContent ? undefined : r.snippetRange?.startLine,
471
- endLine: usedFullContent ? undefined : r.snippetRange?.endLine,
472
- });
548
+ citations.push(
549
+ attachCitationTraceMetadata(
550
+ {
551
+ docid: r.docid,
552
+ uri: r.uri,
553
+ startLine: passage.startLine,
554
+ endLine: passage.endLine,
555
+ },
556
+ {
557
+ sourceHash,
558
+ mirrorHash,
559
+ passageHash: passage.passageHash,
560
+ seq: plannerMetadata?.seq,
561
+ rank: finalRanks.get(r) ?? 1,
562
+ plannerRank: plannerMetadata?.retrievalRank,
563
+ sources: plannerMetadata?.sources,
564
+ graphExpanded: plannerMetadata?.graphExpanded,
565
+ }
566
+ )
567
+ );
473
568
  }
474
569
 
475
570
  if (promptSources.length === 0) {
@@ -528,3 +623,49 @@ export function processAnswerResult(rawResult: AnswerGenerationResult): {
528
623
  answerContext: rawResult.answerContext,
529
624
  };
530
625
  }
626
+
627
+ /** Process first, then record only citations retained in the final answer. */
628
+ export async function processAnswerResultWithTrace(
629
+ rawResult: AnswerGenerationResult,
630
+ traceSession?: RetrievalTraceSession
631
+ ): Promise<ReturnType<typeof processAnswerResult>> {
632
+ const processed = processAnswerResult(rawResult);
633
+ if (!traceSession || processed.citations.length === 0) return processed;
634
+ const evidence = processed.citations.flatMap((citation) => {
635
+ const metadata = citation[CITATION_TRACE_METADATA];
636
+ if (
637
+ !metadata ||
638
+ citation.startLine === undefined ||
639
+ citation.endLine === undefined
640
+ ) {
641
+ return [];
642
+ }
643
+ return [
644
+ {
645
+ docid: citation.docid,
646
+ uri: citation.uri,
647
+ sourceHash: metadata.sourceHash,
648
+ mirrorHash: metadata.mirrorHash,
649
+ passageHash: metadata.passageHash,
650
+ startLine: citation.startLine,
651
+ endLine: citation.endLine,
652
+ rank: metadata.rank,
653
+ ...(metadata.seq === undefined ? {} : { seq: metadata.seq }),
654
+ ...(metadata.plannerRank === undefined
655
+ ? {}
656
+ : { plannerRank: metadata.plannerRank }),
657
+ ...(metadata.sources === undefined
658
+ ? {}
659
+ : { sources: metadata.sources }),
660
+ ...(metadata.graphExpanded === undefined
661
+ ? {}
662
+ : { graphExpanded: metadata.graphExpanded }),
663
+ },
664
+ ];
665
+ });
666
+ const recorded = await traceSession.recordEvidence("cite", evidence);
667
+ if (!recorded.ok) {
668
+ throw new Error(`Trace recording failed: ${recorded.error.message}`);
669
+ }
670
+ return processed;
671
+ }
@@ -0,0 +1,235 @@
1
+ import { z } from "zod";
2
+
3
+ export const CLAIM_VERIFICATION_SCHEMA_VERSION = "1.0" as const;
4
+ export const CLAIM_COORDINATE_SPACE = "utf16_code_units" as const;
5
+
6
+ const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
7
+
8
+ export const semanticClaimJudgmentSchema = z
9
+ .object({
10
+ claimId: sha256Schema,
11
+ verdict: z.enum(["supported", "contradicted"]),
12
+ confidence: z.number().min(0).max(1),
13
+ evidenceIds: z.array(sha256Schema).min(1).max(256),
14
+ rationaleCode: z.enum(["semantic_entailment", "semantic_contradiction"]),
15
+ verifierFingerprint: sha256Schema,
16
+ })
17
+ .strict()
18
+ .superRefine((value, context) => {
19
+ const expected =
20
+ value.verdict === "supported"
21
+ ? "semantic_entailment"
22
+ : "semantic_contradiction";
23
+ if (
24
+ value.rationaleCode !== expected ||
25
+ new Set(value.evidenceIds).size !== value.evidenceIds.length
26
+ ) {
27
+ context.addIssue({
28
+ code: "custom",
29
+ message: "semantic judgment is incoherent",
30
+ });
31
+ }
32
+ });
33
+
34
+ export type SemanticClaimJudgment = z.infer<typeof semanticClaimJudgmentSchema>;
35
+ const evidenceReferenceSchema = z
36
+ .object({
37
+ evidenceId: sha256Schema,
38
+ uri: z.string().min(1).max(2048).startsWith("gno://"),
39
+ startLine: z.number().int().positive(),
40
+ endLine: z.number().int().positive(),
41
+ text: z.string().min(1),
42
+ sourceHash: sha256Schema,
43
+ mirrorHash: sha256Schema,
44
+ passageHash: sha256Schema,
45
+ })
46
+ .strict()
47
+ .refine((value) => value.endLine >= value.startLine, {
48
+ message: "evidence line range is reversed",
49
+ path: ["endLine"],
50
+ });
51
+
52
+ const rejectedCitationSchema = z
53
+ .object({
54
+ marker: z.string().min(1),
55
+ start: z.number().int().nonnegative(),
56
+ end: z.number().int().positive(),
57
+ evidenceId: sha256Schema.nullable(),
58
+ reason: z.enum([
59
+ "malformed_citation",
60
+ "out_of_capsule",
61
+ "freshness_unavailable",
62
+ "freshness_receipt_invalid",
63
+ "freshness_receipt_mismatch",
64
+ "evidence_stale",
65
+ "evidence_missing",
66
+ "orphan_citation",
67
+ ]),
68
+ })
69
+ .strict()
70
+ .refine((value) => value.end > value.start, {
71
+ message: "citation span must be non-empty",
72
+ path: ["end"],
73
+ });
74
+
75
+ const verifiedClaimSchema = z
76
+ .object({
77
+ claimId: sha256Schema,
78
+ text: z.string().min(1),
79
+ start: z.number().int().nonnegative(),
80
+ end: z.number().int().positive(),
81
+ status: z.enum(["supported", "contradicted", "insufficient", "uncertain"]),
82
+ confidence: z.number().min(0).max(1).nullable(),
83
+ rationaleCode: z.enum([
84
+ "semantic_entailment",
85
+ "semantic_contradiction",
86
+ "no_valid_evidence",
87
+ "semantic_judgment_unavailable",
88
+ ]),
89
+ verifierFingerprint: sha256Schema.nullable(),
90
+ evidence: z.array(evidenceReferenceSchema).max(256),
91
+ rejectedCitations: z.array(rejectedCitationSchema).max(256),
92
+ })
93
+ .strict()
94
+ .superRefine((value, context) => {
95
+ if (value.end <= value.start) {
96
+ context.addIssue({
97
+ code: "custom",
98
+ message: "claim span must be non-empty",
99
+ path: ["end"],
100
+ });
101
+ }
102
+ const semantic =
103
+ value.status === "supported" || value.status === "contradicted";
104
+ const expectedRationale = {
105
+ supported: "semantic_entailment",
106
+ contradicted: "semantic_contradiction",
107
+ insufficient: "no_valid_evidence",
108
+ uncertain: "semantic_judgment_unavailable",
109
+ }[value.status];
110
+ const evidenceCountValid =
111
+ value.status === "insufficient"
112
+ ? value.evidence.length === 0
113
+ : value.evidence.length > 0;
114
+ if (
115
+ value.rationaleCode !== expectedRationale ||
116
+ !evidenceCountValid ||
117
+ (semantic &&
118
+ (value.confidence === null || value.verifierFingerprint === null)) ||
119
+ (!semantic &&
120
+ (value.confidence !== null || value.verifierFingerprint !== null))
121
+ ) {
122
+ context.addIssue({
123
+ code: "custom",
124
+ message: "claim status fields are incoherent",
125
+ });
126
+ }
127
+ const ids = value.evidence.map((item) => item.evidenceId);
128
+ if (new Set(ids).size !== ids.length) {
129
+ context.addIssue({
130
+ code: "custom",
131
+ message: "claim evidence must be unique",
132
+ path: ["evidence"],
133
+ });
134
+ }
135
+ });
136
+
137
+ const coverageSchema = z
138
+ .object({
139
+ totalClaims: z.number().int().nonnegative(),
140
+ supportedClaims: z.number().int().nonnegative(),
141
+ contradictedClaims: z.number().int().nonnegative(),
142
+ insufficientClaims: z.number().int().nonnegative(),
143
+ uncertainClaims: z.number().int().nonnegative(),
144
+ supportedRatio: z.number().min(0).max(1),
145
+ })
146
+ .strict();
147
+
148
+ export const claimVerificationResultSchema = z
149
+ .object({
150
+ schemaVersion: z.literal(CLAIM_VERIFICATION_SCHEMA_VERSION),
151
+ coordinateSpace: z.literal(CLAIM_COORDINATE_SPACE),
152
+ capsuleId: sha256Schema,
153
+ answerHash: sha256Schema,
154
+ coverageThreshold: z.literal(1),
155
+ claims: z.array(verifiedClaimSchema).max(256),
156
+ rejectedCitations: z.array(rejectedCitationSchema).max(256),
157
+ coverage: coverageSchema,
158
+ answerStatus: z.enum(["verified", "abstained"]),
159
+ abstained: z.boolean(),
160
+ abstentionReason: z
161
+ .enum([
162
+ "contradiction_detected",
163
+ "coverage_below_threshold",
164
+ "no_substantive_claims",
165
+ "citation_hygiene_failed",
166
+ ])
167
+ .nullable(),
168
+ abstentionText: z.string().min(1).nullable(),
169
+ })
170
+ .strict()
171
+ .superRefine((value, context) => {
172
+ const counts = {
173
+ supportedClaims: value.claims.filter(
174
+ (claim) => claim.status === "supported"
175
+ ).length,
176
+ contradictedClaims: value.claims.filter(
177
+ (claim) => claim.status === "contradicted"
178
+ ).length,
179
+ insufficientClaims: value.claims.filter(
180
+ (claim) => claim.status === "insufficient"
181
+ ).length,
182
+ uncertainClaims: value.claims.filter(
183
+ (claim) => claim.status === "uncertain"
184
+ ).length,
185
+ };
186
+ const supportedRatio =
187
+ value.claims.length === 0
188
+ ? 0
189
+ : counts.supportedClaims / value.claims.length;
190
+ const expectedReason =
191
+ value.claims.length === 0
192
+ ? "no_substantive_claims"
193
+ : counts.contradictedClaims > 0
194
+ ? "contradiction_detected"
195
+ : value.rejectedCitations.length > 0 ||
196
+ value.claims.some((claim) => claim.rejectedCitations.length > 0)
197
+ ? "citation_hygiene_failed"
198
+ : supportedRatio < 1
199
+ ? "coverage_below_threshold"
200
+ : null;
201
+ const coverageValid =
202
+ value.coverage.totalClaims === value.claims.length &&
203
+ Object.entries(counts).every(
204
+ ([key, count]) => value.coverage[key as keyof typeof counts] === count
205
+ ) &&
206
+ value.coverage.supportedRatio === supportedRatio;
207
+ const abstentionValid =
208
+ value.abstentionReason === expectedReason &&
209
+ value.abstained === (expectedReason !== null) &&
210
+ value.answerStatus ===
211
+ (expectedReason === null ? "verified" : "abstained") &&
212
+ (expectedReason === null
213
+ ? value.abstentionText === null
214
+ : value.abstentionText !== null);
215
+ const spansValid = value.claims.every(
216
+ (claim, index) =>
217
+ index === 0 || claim.start >= (value.claims[index - 1]?.end ?? 0)
218
+ );
219
+ const claimIds = value.claims.map((claim) => claim.claimId);
220
+ if (
221
+ !coverageValid ||
222
+ !abstentionValid ||
223
+ !spansValid ||
224
+ new Set(claimIds).size !== claimIds.length
225
+ ) {
226
+ context.addIssue({
227
+ code: "custom",
228
+ message: "claim verification aggregate is incoherent",
229
+ });
230
+ }
231
+ });
232
+
233
+ export type ClaimVerificationResult = z.infer<
234
+ typeof claimVerificationResultSchema
235
+ >;