@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.
- package/README.md +14 -7
- package/assets/skill/SKILL.md +54 -12
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +2 -1
- package/spec/AGENTS.md +83 -0
- package/spec/CLAUDE.md +83 -0
- package/spec/bench-fixture.schema.json +137 -0
- package/spec/cli.md +2919 -0
- package/spec/db/schema.sql +442 -0
- package/spec/evals-agentic.md +592 -0
- package/spec/evals.md +1106 -0
- package/spec/mcp.md +2279 -0
- package/spec/output-schemas/activation-verification.schema.json +515 -0
- package/spec/output-schemas/ask.schema.json +564 -0
- package/spec/output-schemas/backlinks.schema.json +131 -0
- package/spec/output-schemas/bench-result.schema.json +120 -0
- package/spec/output-schemas/capture-receipt.schema.json +143 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/collection-list.schema.json +45 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
- package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
- package/spec/output-schemas/context-list.schema.json +21 -0
- package/spec/output-schemas/doctor.schema.json +313 -0
- package/spec/output-schemas/error.schema.json +30 -0
- package/spec/output-schemas/expansion.schema.json +37 -0
- package/spec/output-schemas/get.schema.json +140 -0
- package/spec/output-schemas/graph-query.schema.json +99 -0
- package/spec/output-schemas/graph.schema.json +371 -0
- package/spec/output-schemas/links-list.schema.json +186 -0
- package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
- package/spec/output-schemas/mcp-http-error.schema.json +30 -0
- package/spec/output-schemas/mcp-job-list.schema.json +58 -0
- package/spec/output-schemas/mcp-job-status.schema.json +224 -0
- package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
- package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
- package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
- package/spec/output-schemas/models-list.schema.json +93 -0
- package/spec/output-schemas/multi-get.schema.json +103 -0
- package/spec/output-schemas/process-status.schema.json +119 -0
- package/spec/output-schemas/query-diagnose.schema.json +123 -0
- package/spec/output-schemas/resident-status.schema.json +154 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
- package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
- package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
- package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
- package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
- package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
- package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
- package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
- package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
- package/spec/output-schemas/search-result.schema.json +154 -0
- package/spec/output-schemas/search-results.schema.json +338 -0
- package/spec/output-schemas/similar.schema.json +84 -0
- package/spec/output-schemas/status.schema.json +676 -0
- package/spec/output-schemas/tags-list.schema.json +48 -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 +7 -0
- package/src/app/context-runtime.ts +20 -2
- 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 +144 -183
- package/src/cli/commands/context-build.ts +56 -9
- package/src/cli/commands/get.ts +64 -3
- package/src/cli/commands/query.ts +62 -23
- package/src/cli/commands/replay.ts +140 -0
- package/src/cli/commands/search.ts +48 -3
- package/src/cli/commands/shared.ts +3 -1
- package/src/cli/commands/trace.ts +200 -0
- package/src/cli/commands/vsearch.ts +75 -53
- package/src/cli/program.ts +287 -1
- package/src/config/index.ts +9 -0
- package/src/config/retrieval-traces.ts +56 -0
- package/src/config/types.ts +4 -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 +44 -25
- package/src/core/context-evidence.ts +6 -0
- package/src/core/retrieval-qrels.ts +405 -0
- package/src/core/retrieval-replay-candidate.ts +368 -0
- package/src/core/retrieval-replay-types.ts +109 -0
- package/src/core/retrieval-replay-validation.ts +89 -0
- package/src/core/retrieval-replay.ts +441 -0
- package/src/core/retrieval-trace-evidence-origin.ts +178 -0
- package/src/core/retrieval-trace-export.ts +113 -0
- package/src/core/retrieval-trace-filter-normalization.ts +27 -0
- package/src/core/retrieval-trace-filters.ts +19 -0
- package/src/core/retrieval-trace-management-helpers.ts +247 -0
- package/src/core/retrieval-trace-management-types.ts +132 -0
- package/src/core/retrieval-trace-management.ts +422 -0
- package/src/core/retrieval-trace-request.ts +141 -0
- package/src/core/retrieval-trace-session.ts +507 -0
- package/src/core/retrieval-trace.ts +472 -0
- 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/context.ts +87 -15
- package/src/mcp/tools/get.ts +35 -1
- package/src/mcp/tools/index.ts +83 -0
- package/src/mcp/tools/query.ts +95 -64
- package/src/mcp/tools/search.ts +36 -13
- package/src/mcp/tools/trace.ts +143 -0
- package/src/mcp/tools/vsearch.ts +71 -38
- package/src/pipeline/answer.ts +167 -26
- 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/graph-retrieval.ts +15 -1
- package/src/pipeline/hybrid.ts +151 -43
- package/src/pipeline/search.ts +36 -3
- package/src/pipeline/trace-metadata.ts +47 -0
- package/src/pipeline/types.ts +68 -0
- package/src/pipeline/vsearch.ts +101 -38
- package/src/sdk/client.ts +415 -73
- package/src/sdk/documents.ts +48 -1
- package/src/sdk/index.ts +17 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/context-capsule.ts +67 -8
- package/src/serve/public/app.tsx +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/public/pages/Dashboard.tsx +10 -0
- package/src/serve/public/pages/TraceHistory.tsx +478 -0
- package/src/serve/public/pages/trace-history-detail.tsx +224 -0
- package/src/serve/retrieval-trace.ts +28 -0
- package/src/serve/routes/api.ts +508 -68
- package/src/serve/routes/traces.ts +156 -0
- package/src/serve/server.ts +87 -2
- package/src/store/index.ts +31 -0
- package/src/store/migrations/014-retrieval-traces.ts +303 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/retrieval-trace-codec.ts +384 -0
- package/src/store/sqlite/adapter.ts +153 -1
- package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
- package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
- package/src/store/sqlite/retrieval-trace-store.ts +515 -0
- package/src/store/types.ts +297 -0
- package/src/store/vector/sqlite-vec.ts +76 -1
- package/src/store/vector/types.ts +1 -1
package/src/pipeline/answer.ts
CHANGED
|
@@ -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
|
|
432
|
-
|
|
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 &&
|
|
436
|
-
const contentResult = await store.getContent(
|
|
510
|
+
if (store && mirrorHash) {
|
|
511
|
+
const contentResult = await store.getContent(mirrorHash);
|
|
437
512
|
if (contentResult.ok && contentResult.value) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
>;
|