@gmickel/gno 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +28 -8
  2. package/assets/skill/SKILL.md +73 -27
  3. package/assets/skill/mcp-reference.md +7 -2
  4. package/assets/skill/recipes/citation-and-provenance.md +32 -9
  5. package/package.json +1 -1
  6. package/spec/cli.md +142 -17
  7. package/spec/db/schema.sql +170 -0
  8. package/spec/evals-agentic.md +87 -5
  9. package/spec/mcp.md +75 -3
  10. package/spec/output-schemas/ask.schema.json +198 -0
  11. package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
  12. package/spec/output-schemas/changes.schema.json +280 -0
  13. package/spec/output-schemas/claim-verification.schema.json +291 -0
  14. package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
  15. package/spec/output-schemas/document-diff.schema.json +185 -0
  16. package/spec/output-schemas/impact.schema.json +122 -0
  17. package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
  18. package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
  19. package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
  20. package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
  21. package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
  22. package/src/app/context-runtime-contract.ts +10 -5
  23. package/src/app/context-runtime-input.ts +29 -1
  24. package/src/app/context-runtime-types.ts +4 -0
  25. package/src/app/context-runtime.ts +5 -1
  26. package/src/app/context-surface.ts +4 -0
  27. package/src/app/verified-ask.ts +291 -0
  28. package/src/cli/commands/ask-format.ts +255 -0
  29. package/src/cli/commands/ask.ts +40 -149
  30. package/src/cli/commands/changes.ts +160 -0
  31. package/src/cli/commands/context-saved.ts +189 -0
  32. package/src/cli/options.ts +8 -0
  33. package/src/cli/program.ts +227 -1
  34. package/src/core/capsule-registry.ts +279 -0
  35. package/src/core/capsule-reverification-scheduler.ts +218 -0
  36. package/src/core/capsule-reverification.ts +289 -0
  37. package/src/core/change-diff.ts +182 -0
  38. package/src/core/change-journal.ts +228 -0
  39. package/src/core/context-budget.ts +6 -0
  40. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  41. package/src/core/context-capsule-schema.ts +17 -0
  42. package/src/core/context-capsule-validation.ts +3 -2
  43. package/src/core/context-capsule.ts +18 -0
  44. package/src/core/context-compiler.ts +33 -21
  45. package/src/core/context-evidence.ts +6 -0
  46. package/src/core/knowledge-delta.ts +395 -0
  47. package/src/core/knowledge-impact.ts +202 -0
  48. package/src/core/retrieval-trace-evidence-origin.ts +3 -0
  49. package/src/core/retrieval-trace-session.ts +15 -2
  50. package/src/ingestion/sync.ts +214 -165
  51. package/src/llm/errors.ts +10 -1
  52. package/src/llm/httpGeneration.ts +11 -1
  53. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  54. package/src/llm/types.ts +6 -0
  55. package/src/mcp/tools/ask.ts +228 -0
  56. package/src/mcp/tools/changes.ts +80 -0
  57. package/src/mcp/tools/context.ts +28 -7
  58. package/src/mcp/tools/index.ts +38 -0
  59. package/src/pipeline/claim-verification-schema.ts +235 -0
  60. package/src/pipeline/claim-verification.ts +487 -0
  61. package/src/pipeline/claim-verifier.ts +474 -0
  62. package/src/pipeline/types.ts +25 -0
  63. package/src/sdk/client.ts +77 -2
  64. package/src/sdk/index.ts +7 -0
  65. package/src/sdk/types.ts +22 -0
  66. package/src/serve/doc-events.ts +12 -1
  67. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  68. package/src/serve/public/globals.built.css +1 -1
  69. package/src/serve/public/pages/Ask.tsx +42 -4
  70. package/src/serve/resident-runtime.ts +22 -0
  71. package/src/serve/routes/api.ts +162 -3
  72. package/src/serve/routes/changes.ts +102 -0
  73. package/src/serve/server.ts +34 -0
  74. package/src/serve/watch-service.ts +9 -0
  75. package/src/store/index.ts +21 -0
  76. package/src/store/migrations/015-document-change-journal.ts +85 -0
  77. package/src/store/migrations/016-saved-capsules.ts +131 -0
  78. package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
  79. package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
  80. package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
  81. package/src/store/migrations/index.ts +10 -0
  82. package/src/store/sqlite/adapter.ts +291 -7
  83. package/src/store/sqlite/capsule-registry-store.ts +534 -0
  84. package/src/store/sqlite/change-journal-store.ts +473 -0
  85. package/src/store/types.ts +262 -0
@@ -0,0 +1,228 @@
1
+ /** MCP verified Ask tool over the shared closed-evidence application boundary. */
2
+
3
+ import { z } from "zod";
4
+
5
+ import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
6
+ import type { QueryModeInput } from "../../pipeline/types";
7
+ import type { ToolContext } from "../server";
8
+ import type { ToolResult } from "./index";
9
+
10
+ import { buildVerifiedAsk } from "../../app/verified-ask";
11
+ import {
12
+ finishRetrievalTraceAfterError,
13
+ retrievalTraceFilters,
14
+ startRetrievalTraceRequest,
15
+ } from "../../core/retrieval-trace-request";
16
+ import { attachRetrievalTraceMetadata } from "../../core/retrieval-trace-session";
17
+ import { normalizeStructuredQueryInput } from "../../core/structured-query";
18
+ import { resolveModelUri } from "../../llm/registry";
19
+ import { answerTraceTerminalStatus } from "../../pipeline/answer";
20
+ import { createMcpModelPorts, type McpModelPortFactory } from "./context";
21
+ import { normalizeTagFilters, runTool } from "./index";
22
+
23
+ const queryModeSchema = z
24
+ .object({
25
+ mode: z.enum(["term", "intent", "hyde"]),
26
+ text: z.string().trim().min(1),
27
+ })
28
+ .strict();
29
+
30
+ export const askInputSchema = z
31
+ .object({
32
+ query: z.string().trim().min(1),
33
+ verify: z.literal(true),
34
+ collection: z.string().optional(),
35
+ limit: z.number().int().min(1).max(100).default(5),
36
+ minScore: z.number().min(0).max(1).optional(),
37
+ lang: z.string().optional(),
38
+ intent: z.string().optional(),
39
+ candidateLimit: z.number().int().min(1).max(100).optional(),
40
+ exclude: z.array(z.string()).optional(),
41
+ queryModes: z.array(queryModeSchema).optional(),
42
+ tagsAll: z.array(z.string()).optional(),
43
+ tagsAny: z.array(z.string()).optional(),
44
+ since: z.string().optional(),
45
+ until: z.string().optional(),
46
+ categories: z.array(z.string()).optional(),
47
+ author: z.string().optional(),
48
+ graph: z.boolean().optional(),
49
+ noGraph: z.boolean().optional(),
50
+ noRerank: z.boolean().optional(),
51
+ maxAnswerTokens: z.number().int().positive().optional(),
52
+ contextBudgetTokens: z.number().int().positive().optional(),
53
+ contextBudgetBytes: z.number().int().positive().optional(),
54
+ })
55
+ .strict();
56
+
57
+ type AskInput = z.infer<typeof askInputSchema>;
58
+
59
+ export interface HandleAskDependencies {
60
+ modelPortFactory?: McpModelPortFactory;
61
+ }
62
+
63
+ const exactSpan = (uri: string, startLine: number, endLine: number): string =>
64
+ `${uri}:L${startLine}${startLine === endLine ? "" : `-L${endLine}`}`;
65
+
66
+ export const formatVerifiedAskReadable = (
67
+ result: Awaited<ReturnType<typeof buildVerifiedAsk>>
68
+ ): string => {
69
+ const numbers = new Map(
70
+ (result.citations ?? []).flatMap((citation, index) =>
71
+ citation.evidenceId ? [[citation.evidenceId, index + 1] as const] : []
72
+ )
73
+ );
74
+ const answer = (result.answer ?? "").replace(
75
+ /\[evidence:([a-f0-9]{64})\]/g,
76
+ (_marker, evidenceId: string) => {
77
+ const number = numbers.get(evidenceId);
78
+ return number === undefined ? "" : `[${number}]`;
79
+ }
80
+ );
81
+ const claims = result.verification?.claims;
82
+ const lines = [
83
+ answer || "No verified answer.",
84
+ "",
85
+ `Verification: ${claims?.answerStatus ?? "unavailable"}`,
86
+ ];
87
+ if (claims?.abstentionReason) {
88
+ lines.push(`Reason: ${claims.abstentionReason}`);
89
+ }
90
+ if (claims) {
91
+ lines.push(
92
+ `Claims: ${claims.coverage.supportedClaims}/${claims.coverage.totalClaims} supported`
93
+ );
94
+ }
95
+ const semantic = result.verification?.semantic;
96
+ if (semantic) {
97
+ lines.push(`Semantic verifier: ${semantic.status} (${semantic.reason})`);
98
+ }
99
+ for (const claim of claims?.claims ?? []) {
100
+ lines.push(`- ${claim.status}: ${claim.text}`);
101
+ for (const evidence of claim.evidence) {
102
+ lines.push(
103
+ ` ${exactSpan(evidence.uri, evidence.startLine, evidence.endLine)} (${evidence.evidenceId})`
104
+ );
105
+ }
106
+ }
107
+ for (const gap of result.verification?.capsule.coverage.gaps ?? []) {
108
+ lines.push(`Gap: ${gap.facet} (${gap.code})`);
109
+ }
110
+ for (const facet of result.verification?.capsule.coverage.unresolvedFacets ??
111
+ []) {
112
+ lines.push(`Unresolved facet: ${facet}`);
113
+ }
114
+ for (const [name, state] of Object.entries(
115
+ result.verification?.capsule.retrieval.capabilityStates ?? {}
116
+ )) {
117
+ if (state.requested && state.outcome !== "used") {
118
+ lines.push(
119
+ `Capability: ${name} ${state.outcome}${state.fallbackReasons.length > 0 ? ` (${state.fallbackReasons.join(", ")})` : ""}`
120
+ );
121
+ }
122
+ }
123
+ for (const [index, citation] of (result.citations ?? []).entries()) {
124
+ const span =
125
+ citation.startLine === undefined || citation.endLine === undefined
126
+ ? citation.uri
127
+ : exactSpan(citation.uri, citation.startLine, citation.endLine);
128
+ lines.push(`[${index + 1}] ${span} (${citation.evidenceId ?? "unknown"})`);
129
+ }
130
+ return lines.join("\n");
131
+ };
132
+
133
+ export const handleAsk = (
134
+ args: AskInput,
135
+ context: ToolContext,
136
+ dependencies: HandleAskDependencies = {}
137
+ ): Promise<ToolResult> =>
138
+ runTool(
139
+ context,
140
+ "gno_ask",
141
+ async () => {
142
+ if (
143
+ args.collection &&
144
+ !context.collections.some(
145
+ (collection) => collection.name === args.collection
146
+ )
147
+ ) {
148
+ throw new Error(`Collection not found: ${args.collection}`);
149
+ }
150
+ const normalized = normalizeStructuredQueryInput(
151
+ args.query,
152
+ (args.queryModes ?? []) as QueryModeInput[]
153
+ );
154
+ if (!normalized.ok) throw new Error(normalized.error.message);
155
+ const query = normalized.value.query;
156
+ const options = {
157
+ ...args,
158
+ queryModes:
159
+ normalized.value.queryModes.length > 0
160
+ ? normalized.value.queryModes
161
+ : undefined,
162
+ tagsAll: normalizeTagFilters(args.tagsAll),
163
+ tagsAny: normalizeTagFilters(args.tagsAny),
164
+ };
165
+ let traceSession: RetrievalTraceSession | undefined;
166
+ let modelPorts: Awaited<ReturnType<typeof createMcpModelPorts>> | null =
167
+ null;
168
+ try {
169
+ const started = await startRetrievalTraceRequest({
170
+ store: context.store,
171
+ config: context.config,
172
+ query,
173
+ filters: retrievalTraceFilters(options),
174
+ pipeline: "ask",
175
+ indexName: context.indexName,
176
+ modelUris: [
177
+ resolveModelUri(
178
+ context.config,
179
+ "embed",
180
+ undefined,
181
+ args.collection
182
+ ),
183
+ resolveModelUri(
184
+ context.config,
185
+ "rerank",
186
+ undefined,
187
+ args.collection
188
+ ),
189
+ resolveModelUri(context.config, "gen", undefined, args.collection),
190
+ ],
191
+ });
192
+ if (!started.ok) throw new Error(started.error.message);
193
+ traceSession = started.value ?? undefined;
194
+ modelPorts = await createMcpModelPorts(
195
+ context,
196
+ args.collection,
197
+ dependencies.modelPortFactory,
198
+ { generation: true }
199
+ );
200
+ if (!modelPorts.genPort) {
201
+ throw new Error(
202
+ "Answer generation requested but no generation model is available"
203
+ );
204
+ }
205
+ const result = await buildVerifiedAsk(query, options, {
206
+ store: context.store,
207
+ config: context.config,
208
+ indexName: context.indexName,
209
+ vectorIndex: modelPorts.vectorIndex,
210
+ embedPort: modelPorts.embedPort,
211
+ rerankPort: modelPorts.rerankPort,
212
+ genPort: modelPorts.genPort,
213
+ traceSession,
214
+ });
215
+ const finished = await traceSession?.finish(
216
+ answerTraceTerminalStatus(result.citations)
217
+ );
218
+ if (finished && !finished.ok) throw new Error(finished.error.message);
219
+ return attachRetrievalTraceMetadata(result, traceSession);
220
+ } catch (error) {
221
+ await finishRetrievalTraceAfterError(traceSession, error);
222
+ throw error;
223
+ } finally {
224
+ await modelPorts?.dispose();
225
+ }
226
+ },
227
+ formatVerifiedAskReadable
228
+ );
@@ -0,0 +1,80 @@
1
+ /** Read-only MCP adapters for knowledge change, diff, and impact services. */
2
+
3
+ import { z } from "zod";
4
+
5
+ import type { ToolContext } from "../server";
6
+
7
+ import {
8
+ analyzeKnowledgeImpact,
9
+ getKnowledgeDiff,
10
+ listKnowledgeChanges,
11
+ } from "../../core/knowledge-delta";
12
+ import { runTool, type ToolResult } from "./index";
13
+
14
+ export const changesInputSchema = z.object({
15
+ since: z.string().trim().min(1).max(512).optional(),
16
+ collection: z.string().trim().min(1).max(256).optional(),
17
+ limit: z.number().int().min(1).max(1000).default(100),
18
+ });
19
+
20
+ export const diffInputSchema = z.object({
21
+ ref: z.string().trim().min(1).max(4096),
22
+ change: z.string().trim().min(1).max(512).optional(),
23
+ });
24
+
25
+ export const impactInputSchema = z.object({
26
+ ref: z.string().trim().min(1).max(4096),
27
+ maxDepth: z.number().int().min(1).max(6).default(3),
28
+ maxNodes: z.number().int().min(1).max(1000).default(100),
29
+ maxEdges: z.number().int().min(1).max(5000).default(250),
30
+ frontierLimit: z.number().int().min(1).max(1000).default(100),
31
+ visitedLimit: z.number().int().min(1).max(5000).default(500),
32
+ });
33
+
34
+ const unwrap = <T>(
35
+ result:
36
+ | { success: true; data: T }
37
+ | { success: false; error: string; isValidation?: boolean }
38
+ ): T => {
39
+ if (result.success) return result.data;
40
+ throw new Error(
41
+ `${result.isValidation ? "VALIDATION" : "RUNTIME"}: ${result.error}`
42
+ );
43
+ };
44
+
45
+ export const handleChanges = (
46
+ args: z.infer<typeof changesInputSchema>,
47
+ ctx: ToolContext
48
+ ): Promise<ToolResult> =>
49
+ runTool(
50
+ ctx,
51
+ "gno_changes",
52
+ async () => unwrap(await listKnowledgeChanges(ctx.store, args)),
53
+ (data) =>
54
+ `${data.changes.length} retained document changes${data.page.truncated ? " (more available)" : ""}`
55
+ );
56
+
57
+ export const handleDiff = (
58
+ args: z.infer<typeof diffInputSchema>,
59
+ ctx: ToolContext
60
+ ): Promise<ToolResult> =>
61
+ runTool(
62
+ ctx,
63
+ "gno_diff",
64
+ async () =>
65
+ unwrap(await getKnowledgeDiff(ctx.store, args.ref, args.change)),
66
+ (data) =>
67
+ `Structural diff for ${data.document.uri}: ${data.status}; history ${data.history.status}; source bodies not retained`
68
+ );
69
+
70
+ export const handleImpact = (
71
+ args: z.infer<typeof impactInputSchema>,
72
+ ctx: ToolContext
73
+ ): Promise<ToolResult> =>
74
+ runTool(
75
+ ctx,
76
+ "gno_impact",
77
+ async () => unwrap(await analyzeKnowledgeImpact(ctx.store, args.ref, args)),
78
+ (data) =>
79
+ `${data.impacted.length} documents depend on ${data.root.uri}${data.meta.truncated ? " (truncated)" : ""}`
80
+ );
@@ -2,7 +2,11 @@
2
2
 
3
3
  import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
4
4
  import type { ModelLease } from "../../llm/nodeLlamaCpp/lifecycle";
5
- import type { EmbeddingPort, RerankPort } from "../../llm/types";
5
+ import type {
6
+ EmbeddingPort,
7
+ GenerationPort,
8
+ RerankPort,
9
+ } from "../../llm/types";
6
10
  import type { VectorIndexPort } from "../../store/vector";
7
11
  import type { ToolContext } from "../server";
8
12
  import type { ToolResult } from "./index";
@@ -85,13 +89,15 @@ const runContextTool = async (
85
89
 
86
90
  interface McpModelPorts {
87
91
  embedPort: EmbeddingPort | null;
92
+ genPort: GenerationPort | null;
88
93
  rerankPort: RerankPort | null;
89
94
  vectorIndex: VectorIndexPort | null;
90
95
  dispose(): Promise<void>;
91
96
  }
92
97
 
93
- interface McpModelPortFactory {
98
+ export interface McpModelPortFactory {
94
99
  createEmbeddingPort: LlmAdapter["createEmbeddingPort"];
100
+ createGenerationPort?: LlmAdapter["createGenerationPort"];
95
101
  createRerankPort: LlmAdapter["createRerankPort"];
96
102
  acquireModelLease?: LlmAdapter["acquireModelLease"];
97
103
  }
@@ -109,7 +115,8 @@ export const disposeContextModelOwners = async (
109
115
  export const createMcpModelPorts = async (
110
116
  context: ToolContext,
111
117
  collection?: string,
112
- factoryOverride?: McpModelPortFactory
118
+ factoryOverride?: McpModelPortFactory,
119
+ options: { generation?: boolean } = {}
113
120
  ): Promise<McpModelPorts> => {
114
121
  const llm = new LlmAdapter(context.config);
115
122
  const factory = factoryOverride ?? llm;
@@ -125,6 +132,7 @@ export const createMcpModelPorts = async (
125
132
  let embedPort: EmbeddingPort | null = null;
126
133
  let ownedEmbedPort: EmbeddingPort | null = null;
127
134
  let rerankPort: RerankPort | null = null;
135
+ let genPort: GenerationPort | null = null;
128
136
  let vectorIndex: VectorIndexPort | null = null;
129
137
  try {
130
138
  const embedResult = await factory.createEmbeddingPort(embedUri, {
@@ -152,14 +160,26 @@ export const createMcpModelPorts = async (
152
160
  }
153
161
  );
154
162
  if (rerankResult.ok) rerankPort = rerankResult.value;
163
+ if (options.generation && factory.createGenerationPort) {
164
+ const genResult = await factory.createGenerationPort(
165
+ resolveModelUri(context.config, "gen", undefined, collection),
166
+ {
167
+ policy,
168
+ onProgress: (value) => progress("gen", value),
169
+ }
170
+ );
171
+ if (genResult.ok) genPort = genResult.value;
172
+ }
155
173
  return {
156
174
  embedPort,
175
+ genPort,
157
176
  rerankPort,
158
177
  vectorIndex,
159
178
  async dispose() {
160
179
  await disposeContextModelOwners(
161
- [ownedEmbedPort, rerankPort].filter(
162
- (port): port is EmbeddingPort | RerankPort => port !== null
180
+ [ownedEmbedPort, rerankPort, genPort].filter(
181
+ (port): port is EmbeddingPort | RerankPort | GenerationPort =>
182
+ port !== null
163
183
  ),
164
184
  lease
165
185
  );
@@ -167,8 +187,9 @@ export const createMcpModelPorts = async (
167
187
  };
168
188
  } catch (error) {
169
189
  await disposeContextModelOwners(
170
- [ownedEmbedPort, rerankPort].filter(
171
- (port): port is EmbeddingPort | RerankPort => port !== null
190
+ [ownedEmbedPort, rerankPort, genPort].filter(
191
+ (port): port is EmbeddingPort | RerankPort | GenerationPort =>
192
+ port !== null
172
193
  ),
173
194
  lease
174
195
  );
@@ -19,7 +19,16 @@ import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
19
19
  import { RETRIEVAL_TRACE_METADATA } from "../../core/retrieval-trace-session";
20
20
  import { normalizeTag } from "../../core/tags";
21
21
  import { handleAddCollection } from "./add-collection";
22
+ import { askInputSchema, handleAsk } from "./ask";
22
23
  import { handleCapture } from "./capture";
24
+ import {
25
+ changesInputSchema,
26
+ diffInputSchema,
27
+ handleChanges,
28
+ handleDiff,
29
+ handleImpact,
30
+ impactInputSchema,
31
+ } from "./changes";
23
32
  import { handleClearCollectionEmbeddings } from "./clear-collection-embeddings";
24
33
  import { handleContext, handleContextVerify } from "./context";
25
34
  import { handleEmbed } from "./embed";
@@ -96,6 +105,7 @@ export const MCP_TOOL_DESCRIPTIONS = {
96
105
  "Compile one deterministic, budgeted, extractive evidence Capsule with exact line spans, coverage gaps, omissions, provenance, and verification fingerprints. Raw search/get tools remain available for manual retrieval.",
97
106
  contextVerify:
98
107
  "Verify a saved Context Capsule without rebuilding or mutating it. Reports unchanged, stale, missing, reranked, and fingerprint drift states against the active index.",
108
+ ask: "Generate one answer from a deterministic Context Capsule, verify every substantive claim against exact retained spans, and abstain unless support coverage is complete. Read-only; returns the Capsule, freshness receipt, claim verdicts, gaps, and evidence IDs.",
99
109
  } as const;
100
110
 
101
111
  /** Tool names whose execution mutates disk, config, or index state. */
@@ -978,6 +988,13 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
978
988
  (args) => handleContextVerify(args, ctx)
979
989
  );
980
990
 
991
+ server.tool(
992
+ "gno_ask",
993
+ MCP_TOOL_DESCRIPTIONS.ask,
994
+ askInputSchema.shape,
995
+ (args) => handleAsk(args, ctx)
996
+ );
997
+
981
998
  server.tool(
982
999
  "gno_search",
983
1000
  MCP_TOOL_DESCRIPTIONS.search,
@@ -1027,6 +1044,27 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1027
1044
  (args) => handleStatus(args, ctx)
1028
1045
  );
1029
1046
 
1047
+ server.tool(
1048
+ "gno_changes",
1049
+ "List retained metadata-only document changes with opaque cursor pagination and retention disclosure.",
1050
+ changesInputSchema.shape,
1051
+ (args) => handleChanges(args, ctx)
1052
+ );
1053
+
1054
+ server.tool(
1055
+ "gno_diff",
1056
+ "Inspect one retained metadata-only structural document change. Source bodies are never returned.",
1057
+ diffInputSchema.shape,
1058
+ (args) => handleDiff(args, ctx)
1059
+ );
1060
+
1061
+ server.tool(
1062
+ "gno_impact",
1063
+ "Find bounded inbound typed, wiki, and Markdown dependencies with deterministic evidence paths.",
1064
+ impactInputSchema.shape,
1065
+ (args) => handleImpact(args, ctx)
1066
+ );
1067
+
1030
1068
  server.tool(
1031
1069
  "gno_trace_list",
1032
1070
  "List bounded metadata-only summaries of private local retrieval traces. Raw replay queries are omitted from history.",
@@ -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
+ >;