@hraness/kb 0.17.1

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 (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,2539 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { createHash, randomBytes } from "node:crypto";
4
+ import { constants as fsConstants } from "node:fs";
5
+ import {
6
+ link,
7
+ lstat,
8
+ open,
9
+ realpath,
10
+ unlink,
11
+ type FileHandle,
12
+ } from "node:fs/promises";
13
+ import {
14
+ basename,
15
+ dirname,
16
+ isAbsolute,
17
+ relative,
18
+ resolve,
19
+ sep,
20
+ } from "node:path";
21
+
22
+ import { z } from "zod";
23
+
24
+ import {
25
+ compilePromotionCorpusAuthoringV2,
26
+ compileRetrievalEvaluationCorpusAuthoringV2,
27
+ type HumanAuthoredEvaluationQuestionV2,
28
+ type PromotionCorpusAuthoringInputV2,
29
+ type PromotionCorpusCompilationResultV2,
30
+ type PromotionCorpusMarkdownDocumentV2,
31
+ } from "./evaluation-corpus-authoring.js";
32
+ import {
33
+ EVALUATION_EVIDENCE_PARSER_VERSION,
34
+ MAX_EVALUATION_EVIDENCE_DOCUMENT_BYTES,
35
+ MAX_EVALUATION_EVIDENCE_DOCUMENTS,
36
+ MAX_EVALUATION_EVIDENCE_TOTAL_BYTES,
37
+ } from "./evaluation-evidence.js";
38
+ import { evaluationImplementationArtifactSha256V2 } from "./evaluation-implementation.js";
39
+ import {
40
+ runGitCommand,
41
+ type GitCommandProvider,
42
+ type GitCommandResult,
43
+ } from "./git.js";
44
+ import {
45
+ MAX_EVALUATION_V2_QUERIES,
46
+ parseRetrievalEvaluationCorpusV2,
47
+ validatePromotionCorpusDesignV2,
48
+ type EvaluationExternalCorpusSealV2,
49
+ type RetrievalEvaluationCorpusV2,
50
+ } from "./evaluation-v2.js";
51
+
52
+ const AUTHORING_DIRECTORY = "kb/evaluations/kb-evidence-routing-v2";
53
+ const MAX_CONFIG_BYTES = 1 * 1_024 * 1_024;
54
+ const MAX_SHARDS = 64;
55
+ const MAX_SHARD_BYTES = 16 * 1_024 * 1_024;
56
+ const MAX_TOTAL_SHARD_BYTES = 64 * 1_024 * 1_024;
57
+ const MAX_OUTPUT_BYTES = 256 * 1_024 * 1_024;
58
+ const MAX_IMPLEMENTATION_SOURCE_BYTES = 4 * 1_024 * 1_024;
59
+ const MAX_IMPLEMENTATION_SOURCES_PER_RETRIEVER = 32;
60
+ const MAX_IMPLEMENTATION_SOURCE_BINDINGS = 64;
61
+ const FROZEN_GIT_TIMEOUT_MS = 30_000;
62
+ const FROZEN_GIT_METADATA_BYTES = 64 * 1_024;
63
+ const MAX_VAULT_LIST_BYTES = 4 * 1_024 * 1_024;
64
+ const MAX_JSON_STRING_BYTES = 1 * 1_024 * 1_024;
65
+ const MAX_SOURCE_FAMILY_RATIONALE_BYTES = 2_048;
66
+ const MAX_SOURCE_FAMILY_REVIEWERS = 32;
67
+ const MAX_SOURCE_FAMILY_REVIEWER_ID_BYTES = 256;
68
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
69
+ const GIT_OBJECT_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
70
+
71
+ const sourceClassSchema = z.enum([
72
+ "authored-note",
73
+ "captured-source",
74
+ "git-history",
75
+ "repository-file",
76
+ ]);
77
+ const trustClassSchema = z.enum([
78
+ "authoritative-current",
79
+ "authoritative-historical",
80
+ "captured-primary",
81
+ "captured-secondary",
82
+ "maintained-synthesis",
83
+ "untrusted-capture",
84
+ ]);
85
+ const evidenceKindSchema = z.enum([
86
+ "frontmatter-field",
87
+ "heading",
88
+ "paragraph",
89
+ "list",
90
+ "table",
91
+ "code-block",
92
+ "pdf-page-span",
93
+ ]);
94
+ const splitSchema = z.enum(["development", "test"]);
95
+ const cohortSchema = z.enum(["caller-seeded", "text-only"]);
96
+ const supportSchema = z.enum(["insufficient", "supported"]);
97
+ const negativeSubtypeSchema = z.enum([
98
+ "boundary-near-miss",
99
+ "conflicting-evidence",
100
+ "missing-required-support",
101
+ "stale-only",
102
+ "topical-near-miss",
103
+ "unknown-entity",
104
+ ]);
105
+ const stratumSchema = z.enum([
106
+ "active-current-state",
107
+ "code-path-context",
108
+ "conceptual-recall",
109
+ "exact-identity",
110
+ "local-context",
111
+ "metadata-constraint",
112
+ "multi-note-relational",
113
+ "no-answer-near-miss",
114
+ "source-provenance",
115
+ "temporal-stale-current",
116
+ ]);
117
+ const laneSchema = z.enum([
118
+ "exact",
119
+ "git",
120
+ "graph",
121
+ "hybrid",
122
+ "keyword",
123
+ "metadata",
124
+ "note",
125
+ "path-context",
126
+ "semantic",
127
+ ]);
128
+ const inputLaneSchema = z.enum(["context", "graph", "history", "metadata", "noteId", "text"]);
129
+ const boundedStringSchema = z.string().refine(
130
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_JSON_STRING_BYTES,
131
+ "string exceeds the authoring byte bound",
132
+ );
133
+ const nonEmptyStringSchema = boundedStringSchema.min(1);
134
+ const sourceFamilyReviewerIdSchema = nonEmptyStringSchema.refine(
135
+ (value) => Buffer.byteLength(value, "utf8") <= MAX_SOURCE_FAMILY_REVIEWER_ID_BYTES,
136
+ `reviewer ID exceeds ${MAX_SOURCE_FAMILY_REVIEWER_ID_BYTES} UTF-8 bytes`,
137
+ );
138
+ const sourceFamilyReviewerIdsSchema = z.array(sourceFamilyReviewerIdSchema)
139
+ .min(2)
140
+ .max(MAX_SOURCE_FAMILY_REVIEWERS)
141
+ .refine((values) => new Set(values).size === values.length, "reviewer IDs must be distinct")
142
+ .refine(
143
+ (values) => values.every((value, index) => value === values.toSorted()[index]),
144
+ "reviewer IDs must be in canonical order",
145
+ );
146
+ const sourceFamilyRationaleSchema = z.string().refine(
147
+ (value) => value.normalize("NFC") === value
148
+ && value.trim() === value
149
+ && !value.includes("\0")
150
+ && Buffer.byteLength(value, "utf8") >= 24
151
+ && Buffer.byteLength(value, "utf8") <= MAX_SOURCE_FAMILY_RATIONALE_BYTES,
152
+ `rationale must be trimmed NFC text from 24 through ${MAX_SOURCE_FAMILY_RATIONALE_BYTES} UTF-8 bytes`,
153
+ );
154
+ const sha256Schema = z.string().regex(SHA256_PATTERN);
155
+ const gitObjectSchema = z.string().regex(GIT_OBJECT_PATTERN);
156
+ const relevanceSchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]);
157
+ const positiveIntegerSchema = z.number().int().positive().safe();
158
+ const nonNegativeIntegerSchema = z.number().int().nonnegative().safe();
159
+
160
+ const lineRangeSchema = z.strictObject({
161
+ start: positiveIntegerSchema,
162
+ end: positiveIntegerSchema,
163
+ });
164
+ const byteRangeSchema = z.strictObject({
165
+ start: nonNegativeIntegerSchema,
166
+ end: nonNegativeIntegerSchema,
167
+ });
168
+ const evidenceSelectorSchema = z.strictObject({
169
+ sourcePath: nonEmptyStringSchema,
170
+ kind: evidenceKindSchema.optional(),
171
+ headingPath: z.array(boundedStringSchema).max(64).optional(),
172
+ heading: boundedStringSchema.optional(),
173
+ exactText: boundedStringSchema.optional(),
174
+ lineRange: lineRangeSchema.optional(),
175
+ expectedUnitId: nonEmptyStringSchema.optional(),
176
+ expectedUnitSha256: sha256Schema.optional(),
177
+ expectedSourceSha256: sha256Schema.optional(),
178
+ expectedByteRange: byteRangeSchema.optional(),
179
+ });
180
+ const documentJudgmentSchema = z.strictObject({
181
+ sourcePath: nonEmptyStringSchema,
182
+ relevance: relevanceSchema,
183
+ });
184
+ const evidenceJudgmentSchema = z.strictObject({
185
+ selector: evidenceSelectorSchema,
186
+ relevance: relevanceSchema,
187
+ });
188
+ const supportSetSchema = z.strictObject({
189
+ key: nonEmptyStringSchema,
190
+ evidence: z.array(evidenceSelectorSchema).max(2_000),
191
+ });
192
+ const nuggetSchema = z.strictObject({
193
+ key: nonEmptyStringSchema,
194
+ text: boundedStringSchema,
195
+ required: z.boolean(),
196
+ acceptableSupportSets: z.array(supportSetSchema).max(100),
197
+ });
198
+ const goldSchema = z.strictObject({
199
+ documents: z.array(documentJudgmentSchema).max(2_000),
200
+ evidenceUnits: z.array(evidenceJudgmentSchema).max(2_000),
201
+ nuggets: z.array(nuggetSchema).max(100),
202
+ });
203
+ const assessorNuggetSchema = z.strictObject({
204
+ nuggetKey: nonEmptyStringSchema,
205
+ required: z.boolean().optional(),
206
+ acceptableSupportSetKeys: z.array(nonEmptyStringSchema).max(100),
207
+ });
208
+ const rawAssessmentSchema = z.strictObject({
209
+ assessorId: nonEmptyStringSchema,
210
+ expectedSupport: supportSchema,
211
+ documents: z.array(documentJudgmentSchema).max(2_000),
212
+ evidenceUnits: z.array(evidenceJudgmentSchema).max(2_000),
213
+ nuggets: z.array(assessorNuggetSchema).max(100),
214
+ });
215
+ const adjudicationSchema = z.discriminatedUnion("status", [
216
+ z.strictObject({ status: z.literal("single-assessor") }),
217
+ z.strictObject({ status: z.literal("agreed") }),
218
+ z.strictObject({
219
+ status: z.literal("resolved"),
220
+ adjudicatorId: nonEmptyStringSchema,
221
+ rationale: nonEmptyStringSchema,
222
+ }),
223
+ ]);
224
+ const metadataFilterSchema = z.discriminatedUnion("kind", [
225
+ z.strictObject({ kind: z.literal("exists"), path: nonEmptyStringSchema }),
226
+ z.strictObject({
227
+ kind: z.literal("equals"),
228
+ path: nonEmptyStringSchema,
229
+ value: z.union([boundedStringSchema, z.number().finite(), z.boolean(), z.null()]),
230
+ }),
231
+ ]);
232
+ const retrievalInputsSchema = z.strictObject({
233
+ text: boundedStringSchema,
234
+ noteId: nonEmptyStringSchema.optional(),
235
+ metadata: z.strictObject({
236
+ filters: z.array(metadataFilterSchema).max(2_000),
237
+ tags: z.array(nonEmptyStringSchema).max(2_000),
238
+ }).optional(),
239
+ graph: z.strictObject({
240
+ seeds: z.array(nonEmptyStringSchema).max(2_000),
241
+ depth: z.union([z.literal(1), z.literal(2)]),
242
+ }).optional(),
243
+ context: z.strictObject({ repositoryPath: nonEmptyStringSchema }).optional(),
244
+ history: z.strictObject({
245
+ query: boundedStringSchema,
246
+ noteIds: z.array(nonEmptyStringSchema).max(2_000),
247
+ }).optional(),
248
+ });
249
+ const inputOriginSchema = z.strictObject({
250
+ lane: inputLaneSchema,
251
+ origin: z.enum(["caller", "query-text"]),
252
+ });
253
+
254
+ export const humanAuthoredEvaluationQuestionV2Schema = z.strictObject({
255
+ key: nonEmptyStringSchema,
256
+ text: boundedStringSchema,
257
+ split: splitSchema,
258
+ cohort: cohortSchema,
259
+ strata: z.array(stratumSchema).min(1).max(10),
260
+ primaryStratum: stratumSchema,
261
+ expectedSupport: supportSchema,
262
+ primaryLane: laneSchema,
263
+ negativeSubtype: negativeSubtypeSchema.optional(),
264
+ inputs: retrievalInputsSchema,
265
+ inputOrigins: z.array(inputOriginSchema).min(1).max(6),
266
+ gold: goldSchema,
267
+ rawAssessments: z.array(rawAssessmentSchema).min(1).max(32),
268
+ adjudication: adjudicationSchema,
269
+ });
270
+
271
+ const authoringDocumentSchema = z.strictObject({
272
+ documentId: nonEmptyStringSchema.optional(),
273
+ sourcePath: nonEmptyStringSchema,
274
+ sourceFamilyKey: nonEmptyStringSchema,
275
+ sourceClass: sourceClassSchema,
276
+ trustClass: trustClassSchema,
277
+ sourceFamilyRationale: sourceFamilyRationaleSchema.optional(),
278
+ sourceFamilyReviewerIds: sourceFamilyReviewerIdsSchema.optional(),
279
+ });
280
+
281
+ const reviewedAuthoringDocumentSchema = authoringDocumentSchema.extend({
282
+ sourceFamilyRationale: sourceFamilyRationaleSchema,
283
+ sourceFamilyReviewerIds: sourceFamilyReviewerIdsSchema,
284
+ });
285
+
286
+ export const kbEvidenceRoutingAuthoringShardSchema = z.strictObject({
287
+ documents: z.array(authoringDocumentSchema).max(MAX_EVALUATION_EVIDENCE_DOCUMENTS),
288
+ questions: z.array(humanAuthoredEvaluationQuestionV2Schema).min(1).max(MAX_EVALUATION_V2_QUERIES),
289
+ });
290
+
291
+ /**
292
+ * Private question specifications deliberately exclude every final or per-assessor label.
293
+ * assignedAssessorIds is the prospective completeness contract for the independent join.
294
+ */
295
+ export const kbEvidenceRoutingPrivateQuestionSpecSchema = z.strictObject({
296
+ key: nonEmptyStringSchema,
297
+ text: boundedStringSchema,
298
+ split: z.literal("test"),
299
+ cohort: cohortSchema,
300
+ strata: z.array(stratumSchema).min(1).max(10),
301
+ primaryStratum: stratumSchema,
302
+ primaryLane: laneSchema,
303
+ negativeSubtype: negativeSubtypeSchema.optional(),
304
+ inputs: retrievalInputsSchema,
305
+ inputOrigins: z.array(inputOriginSchema).min(1).max(6),
306
+ assignedAssessorIds: z.array(nonEmptyStringSchema).min(1).max(32),
307
+ });
308
+
309
+ const privateAssessorJudgmentSchema = z.strictObject({
310
+ questionKey: nonEmptyStringSchema,
311
+ questionSpecSha256: sha256Schema,
312
+ questionSpecShardSha256: sha256Schema,
313
+ expectedSupport: supportSchema,
314
+ documents: z.array(documentJudgmentSchema).max(2_000),
315
+ evidenceUnits: z.array(evidenceJudgmentSchema).max(2_000),
316
+ nuggets: z.array(assessorNuggetSchema).max(100),
317
+ });
318
+
319
+ const privateAdjudicationSchema = z.strictObject({
320
+ questionKey: nonEmptyStringSchema,
321
+ questionSpecSha256: sha256Schema,
322
+ questionSpecShardSha256: sha256Schema,
323
+ expectedSupport: supportSchema,
324
+ gold: goldSchema,
325
+ adjudication: adjudicationSchema,
326
+ });
327
+
328
+ export const kbEvidenceRoutingPrivateQuestionSpecShardSchema = z.strictObject({
329
+ schemaVersion: z.literal(1),
330
+ kind: z.literal("question-specs"),
331
+ buildContractSha256: sha256Schema,
332
+ documents: z.array(reviewedAuthoringDocumentSchema).max(MAX_EVALUATION_EVIDENCE_DOCUMENTS),
333
+ questions: z.array(kbEvidenceRoutingPrivateQuestionSpecSchema)
334
+ .min(1)
335
+ .max(MAX_EVALUATION_V2_QUERIES),
336
+ });
337
+
338
+ export const kbEvidenceRoutingPrivateAssessorJudgmentShardSchema = z.strictObject({
339
+ schemaVersion: z.literal(1),
340
+ kind: z.literal("assessor-judgments"),
341
+ buildContractSha256: sha256Schema,
342
+ assessorId: nonEmptyStringSchema,
343
+ judgments: z.array(privateAssessorJudgmentSchema).min(1).max(MAX_EVALUATION_V2_QUERIES),
344
+ });
345
+
346
+ export const kbEvidenceRoutingPrivateAdjudicationShardSchema = z.strictObject({
347
+ schemaVersion: z.literal(1),
348
+ kind: z.literal("adjudications"),
349
+ buildContractSha256: sha256Schema,
350
+ adjudications: z.array(privateAdjudicationSchema).min(1).max(MAX_EVALUATION_V2_QUERIES),
351
+ });
352
+
353
+ export const kbEvidenceRoutingPrivateAuthoringShardSchema = z.discriminatedUnion("kind", [
354
+ kbEvidenceRoutingPrivateQuestionSpecShardSchema,
355
+ kbEvidenceRoutingPrivateAssessorJudgmentShardSchema,
356
+ kbEvidenceRoutingPrivateAdjudicationShardSchema,
357
+ ]);
358
+
359
+ const assessorSchema = z.strictObject({
360
+ id: nonEmptyStringSchema,
361
+ displayName: nonEmptyStringSchema.optional(),
362
+ affiliation: nonEmptyStringSchema.optional(),
363
+ });
364
+ const minimumUsefulEffectSchema = z.strictObject({
365
+ metric: z.enum([
366
+ "document-recall-at-k",
367
+ "evidence-recall-at-k",
368
+ "false-abstention-rate",
369
+ "no-answer-accuracy",
370
+ "nugget-coverage",
371
+ ]),
372
+ cohort: z.enum(["caller-seeded", "text-only"]),
373
+ minimumAbsoluteDifference: z.number().finite().nonnegative(),
374
+ });
375
+ const nonInferiorityMarginSchema = z.strictObject({
376
+ metric: z.enum([
377
+ "active-current-state-accuracy",
378
+ "code-path-context-accuracy",
379
+ "context-precision",
380
+ "conceptual-recall-accuracy",
381
+ "document-recall-at-k",
382
+ "evidence-recall-at-k",
383
+ "exact-identity-accuracy",
384
+ "local-context-accuracy",
385
+ "metadata-constraint-accuracy",
386
+ "multi-note-relational-accuracy",
387
+ "source-provenance-accuracy",
388
+ "temporal-stale-current-accuracy",
389
+ "four-reader-query-p95-ms",
390
+ "packing-p95-ms",
391
+ "warm-query-p95-ms",
392
+ ]),
393
+ maximumAbsoluteRegression: z.number().finite().nonnegative(),
394
+ maximumRelativeRegression: z.number().finite().nonnegative(),
395
+ });
396
+ const localModelSchema = z.discriminatedUnion("kind", [
397
+ z.strictObject({ kind: z.literal("none") }),
398
+ z.strictObject({ kind: z.literal("model"), id: nonEmptyStringSchema, sha256: sha256Schema }),
399
+ ]);
400
+ const experimentSchema = z.strictObject({
401
+ protocol: z.strictObject({
402
+ minimumUsefulEffects: z.array(minimumUsefulEffectSchema).min(1).max(10),
403
+ nonInferiorityMargins: z.array(nonInferiorityMarginSchema).min(1).max(32),
404
+ contextCeilings: z.strictObject({
405
+ utf8Bytes: positiveIntegerSchema,
406
+ readerTokens: positiveIntegerSchema,
407
+ }),
408
+ pairedPower: z.strictObject({
409
+ alpha: z.number().finite().positive().max(1),
410
+ targetPower: z.number().finite().positive().max(1),
411
+ assumedDiscordantRate: z.number().finite().positive().max(1),
412
+ assumedEffect: z.number().finite().positive().max(1),
413
+ minimumUsefulEffect: z.number().finite().positive().max(1),
414
+ requiredPairs: positiveIntegerSchema,
415
+ }),
416
+ }),
417
+ environment: z.strictObject({
418
+ tokenizer: z.strictObject({ id: nonEmptyStringSchema, sha256: sha256Schema }),
419
+ runtime: z.strictObject({ id: nonEmptyStringSchema, sha256: sha256Schema }),
420
+ hardware: z.strictObject({ id: nonEmptyStringSchema }),
421
+ localModel: localModelSchema,
422
+ cache: z.strictObject({
423
+ preparation: nonEmptyStringSchema,
424
+ fingerprintSha256: sha256Schema,
425
+ }),
426
+ fourReaderBatch: z.strictObject({ id: nonEmptyStringSchema, sha256: sha256Schema }),
427
+ incrementalMutation: z.strictObject({
428
+ sourcePath: nonEmptyStringSchema,
429
+ appendUtf8Sha256: sha256Schema,
430
+ expectedPostMutationSha256: sha256Schema,
431
+ }),
432
+ }),
433
+ });
434
+ const measurementProfileSchema = z.strictObject({
435
+ id: nonEmptyStringSchema,
436
+ operation: z.enum(["cold-index", "four-reader-query", "incremental-update", "packing", "warm-query"]),
437
+ scope: z.enum(["query", "retriever"]),
438
+ cacheState: z.enum(["changed-generation", "cold", "not-applicable", "warm"]),
439
+ concurrency: positiveIntegerSchema,
440
+ repetitions: positiveIntegerSchema,
441
+ });
442
+ const retrieverConfigurationValueSchema = z.union([
443
+ boundedStringSchema,
444
+ z.number().finite(),
445
+ z.boolean(),
446
+ z.null(),
447
+ ]);
448
+ const retrieverSchema = z.strictObject({
449
+ id: nonEmptyStringSchema,
450
+ role: z.enum(["ablation", "baseline", "candidate"]),
451
+ version: nonEmptyStringSchema,
452
+ implementationSha256: sha256Schema,
453
+ lanes: z.array(laneSchema).min(1).max(9),
454
+ configuration: z.record(z.string(), retrieverConfigurationValueSchema),
455
+ });
456
+ const implementationSourceBindingSchema = z.strictObject({
457
+ retrieverId: nonEmptyStringSchema,
458
+ sourcePaths: z.array(nonEmptyStringSchema)
459
+ .min(1)
460
+ .max(MAX_IMPLEMENTATION_SOURCES_PER_RETRIEVER),
461
+ });
462
+
463
+ export const kbEvidenceRoutingEvaluationBuildConfigSchema = z.strictObject({
464
+ schemaVersion: z.literal(1),
465
+ repositoryRoot: nonEmptyStringSchema,
466
+ id: nonEmptyStringSchema,
467
+ description: nonEmptyStringSchema,
468
+ sealedAt: nonEmptyStringSchema,
469
+ frozen: z.strictObject({
470
+ repositoryCommit: gitObjectSchema,
471
+ vaultTree: gitObjectSchema,
472
+ vaultRoot: nonEmptyStringSchema,
473
+ }),
474
+ assessment: z.strictObject({
475
+ rubricVersion: nonEmptyStringSchema,
476
+ assessors: z.array(assessorSchema).min(1).max(64),
477
+ }),
478
+ experiment: experimentSchema,
479
+ measurementProfiles: z.array(measurementProfileSchema).min(1).max(32),
480
+ retrievers: z.array(retrieverSchema).min(1).max(64),
481
+ implementationSources: z.array(implementationSourceBindingSchema)
482
+ .min(1)
483
+ .max(MAX_IMPLEMENTATION_SOURCE_BINDINGS),
484
+ baselineRetrieverId: nonEmptyStringSchema,
485
+ evidenceParserVersion: nonEmptyStringSchema,
486
+ reviewPolicy: z.strictObject({
487
+ ngramSize: z.literal(3).optional(),
488
+ crossSplitNgramThreshold: z.number().finite().positive().max(0.8).optional(),
489
+ labelPredictabilityCeiling: z.number().finite().positive().max(0.65).optional(),
490
+ sourceFamilyAssignment: z.strictObject({
491
+ protocolId: nonEmptyStringSchema,
492
+ protocolSha256: sha256Schema,
493
+ reviewerIds: sourceFamilyReviewerIdsSchema,
494
+ }),
495
+ }),
496
+ shards: z.strictObject({
497
+ /** Development labels are visible in runtime A and may guide candidate selection. */
498
+ development: z.array(nonEmptyStringSchema).min(1).max(MAX_SHARDS),
499
+ /** Visible test-shaped material is QA only and can never support a promotion claim. */
500
+ qa: z.array(nonEmptyStringSchema).max(MAX_SHARDS),
501
+ /** Held-out labels live only under the separately supplied artifact-B root. */
502
+ heldOut: z.array(nonEmptyStringSchema).max(MAX_SHARDS),
503
+ }),
504
+ outputs: z.strictObject({
505
+ corpus: nonEmptyStringSchema,
506
+ externalSeal: nonEmptyStringSchema,
507
+ summary: nonEmptyStringSchema,
508
+ }),
509
+ });
510
+
511
+ export type KbEvidenceRoutingAuthoringShard = z.infer<typeof kbEvidenceRoutingAuthoringShardSchema>;
512
+ export type KbEvidenceRoutingPrivateQuestionSpec = z.infer<
513
+ typeof kbEvidenceRoutingPrivateQuestionSpecSchema
514
+ >;
515
+ export type KbEvidenceRoutingPrivateAuthoringShard = z.infer<
516
+ typeof kbEvidenceRoutingPrivateAuthoringShardSchema
517
+ >;
518
+ export type KbEvidenceRoutingEvaluationBuildConfig = z.infer<
519
+ typeof kbEvidenceRoutingEvaluationBuildConfigSchema
520
+ >;
521
+
522
+ export type KbEvidenceRoutingEvaluationShaSummary = {
523
+ readonly schemaVersion: 1;
524
+ readonly corpus: {
525
+ readonly path: string;
526
+ readonly byteLength: number;
527
+ readonly outputSha256: string;
528
+ readonly committedCorpusSha256: string;
529
+ };
530
+ readonly externalSeal: {
531
+ readonly path: string;
532
+ readonly byteLength: number;
533
+ readonly outputSha256: string;
534
+ };
535
+ readonly authoring: {
536
+ readonly configSha256: string;
537
+ readonly shards: readonly {
538
+ readonly path: string;
539
+ readonly byteLength: number;
540
+ readonly sha256: string;
541
+ }[];
542
+ readonly qaShards: readonly {
543
+ readonly path: string;
544
+ readonly byteLength: number;
545
+ readonly sha256: string;
546
+ }[];
547
+ readonly sources: readonly {
548
+ readonly sourcePath: string;
549
+ readonly byteLength: number;
550
+ readonly sha256: string;
551
+ }[];
552
+ };
553
+ readonly counts: {
554
+ readonly documents: number;
555
+ readonly evidenceUnits: number;
556
+ readonly questions: number;
557
+ };
558
+ readonly visibleReview: {
559
+ readonly questions: number;
560
+ readonly exactQuotaAndBalanceMet: boolean;
561
+ readonly labelPredictabilityMet: boolean;
562
+ readonly pairedPowerMet: boolean;
563
+ readonly diagnosticsSha256: string;
564
+ };
565
+ };
566
+
567
+ export const kbEvidenceRoutingEvaluationShaSummarySchema = z.strictObject({
568
+ schemaVersion: z.literal(1),
569
+ corpus: z.strictObject({
570
+ path: nonEmptyStringSchema,
571
+ byteLength: positiveIntegerSchema,
572
+ outputSha256: sha256Schema,
573
+ committedCorpusSha256: sha256Schema,
574
+ }),
575
+ externalSeal: z.strictObject({
576
+ path: nonEmptyStringSchema,
577
+ byteLength: positiveIntegerSchema,
578
+ outputSha256: sha256Schema,
579
+ }),
580
+ authoring: z.strictObject({
581
+ configSha256: sha256Schema,
582
+ shards: z.array(z.strictObject({
583
+ path: nonEmptyStringSchema,
584
+ byteLength: positiveIntegerSchema,
585
+ sha256: sha256Schema,
586
+ })).max(MAX_SHARDS * 2),
587
+ qaShards: z.array(z.strictObject({
588
+ path: nonEmptyStringSchema,
589
+ byteLength: positiveIntegerSchema,
590
+ sha256: sha256Schema,
591
+ })).max(MAX_SHARDS),
592
+ sources: z.array(z.strictObject({
593
+ sourcePath: nonEmptyStringSchema,
594
+ byteLength: positiveIntegerSchema,
595
+ sha256: sha256Schema,
596
+ })).max(MAX_EVALUATION_EVIDENCE_DOCUMENTS),
597
+ }),
598
+ counts: z.strictObject({
599
+ documents: positiveIntegerSchema,
600
+ evidenceUnits: positiveIntegerSchema,
601
+ questions: positiveIntegerSchema,
602
+ }),
603
+ visibleReview: z.strictObject({
604
+ questions: positiveIntegerSchema,
605
+ exactQuotaAndBalanceMet: z.boolean(),
606
+ labelPredictabilityMet: z.boolean(),
607
+ pairedPowerMet: z.boolean(),
608
+ diagnosticsSha256: sha256Schema,
609
+ }),
610
+ });
611
+
612
+ export type LoadedKbEvidenceRoutingAuthoring = {
613
+ readonly configPath: string;
614
+ readonly configBytes: Buffer;
615
+ readonly config: KbEvidenceRoutingEvaluationBuildConfig;
616
+ readonly repositoryRoot: string;
617
+ readonly input: PromotionCorpusAuthoringInputV2;
618
+ readonly visibleReview: Extract<PromotionCorpusCompilationResultV2, { readonly ok: true }>;
619
+ readonly shardFiles: readonly SecureFile[];
620
+ readonly qaShardFiles: readonly SecureFile[];
621
+ readonly sourceFiles: readonly SourceFile[];
622
+ readonly artifactRoot?: string;
623
+ readonly outputPaths: {
624
+ readonly corpus: string;
625
+ readonly externalSeal: string;
626
+ readonly summary: string;
627
+ };
628
+ };
629
+
630
+ export type BuildKbEvidenceRoutingEvaluationResult = {
631
+ readonly corpus: RetrievalEvaluationCorpusV2;
632
+ readonly externalSeal: Extract<EvaluationExternalCorpusSealV2, { readonly expectedCorpusSha256: string }>;
633
+ readonly summary: KbEvidenceRoutingEvaluationShaSummary;
634
+ readonly summaryOutputSha256: string;
635
+ readonly installs: {
636
+ readonly corpus: "installed" | "unchanged";
637
+ readonly externalSeal: "preexisting";
638
+ readonly summary: "installed" | "unchanged";
639
+ };
640
+ };
641
+
642
+ export type ValidatedKbEvidenceRoutingEvaluationBuild = Readonly<{
643
+ readonly corpus: RetrievalEvaluationCorpusV2;
644
+ readonly externalSeal: Extract<
645
+ EvaluationExternalCorpusSealV2,
646
+ { readonly expectedCorpusSha256: string }
647
+ >;
648
+ readonly summary: KbEvidenceRoutingEvaluationShaSummary;
649
+ readonly summaryOutputSha256: string;
650
+ }>;
651
+
652
+ export type KbEvidenceRoutingEvaluationBuildDependencies = {
653
+ readonly runGit?: GitCommandProvider;
654
+ /** Canonical, disjoint artifact-B root containing held-out shards and all outputs. */
655
+ readonly artifactRoot?: string;
656
+ };
657
+
658
+ export type AnchorKbEvidenceRoutingEvaluationSealResult = {
659
+ readonly externalSeal: Extract<EvaluationExternalCorpusSealV2, { readonly expectedCorpusSha256: string }>;
660
+ readonly outputPath: string;
661
+ readonly outputSha256: string;
662
+ readonly install: "installed" | "unchanged";
663
+ };
664
+
665
+ type SecureFile = {
666
+ readonly absolutePath: string;
667
+ readonly relativePath?: string;
668
+ readonly bytes: Buffer;
669
+ readonly sha256: string;
670
+ };
671
+
672
+ type SourceFile = SecureFile & {
673
+ readonly sourcePath: string;
674
+ readonly markdown: string;
675
+ };
676
+
677
+ type FileIdentity = {
678
+ readonly dev: number;
679
+ readonly ino: number;
680
+ readonly size: number;
681
+ readonly mtimeMs: number;
682
+ readonly ctimeMs: number;
683
+ readonly nlink: number;
684
+ };
685
+
686
+ function formatZodError(error: z.ZodError, label: string): Error {
687
+ const issues = error.issues.map((issue) => {
688
+ const path = issue.path.length === 0 ? label : `${label}.${issue.path.join(".")}`;
689
+ return `${path}: ${issue.message}`;
690
+ });
691
+ return new Error(issues.join("\n"));
692
+ }
693
+
694
+ function parseSchema<S extends z.ZodType>(schema: S, value: unknown, label: string): z.infer<S> {
695
+ const result = schema.safeParse(value);
696
+ if (!result.success) throw formatZodError(result.error, label);
697
+ return result.data;
698
+ }
699
+
700
+ function parseJson(bytes: Buffer, label: string): unknown {
701
+ let source: string;
702
+ try {
703
+ source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
704
+ } catch {
705
+ throw new Error(`${label} must contain valid UTF-8 JSON.`);
706
+ }
707
+ try {
708
+ return JSON.parse(source) as unknown;
709
+ } catch (error) {
710
+ throw new Error(`${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
711
+ }
712
+ }
713
+
714
+ function canonicalJson(value: unknown): string {
715
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
716
+ return JSON.stringify(value);
717
+ }
718
+ if (typeof value === "number") {
719
+ if (!Number.isFinite(value)) throw new TypeError("Canonical JSON cannot contain a non-finite number.");
720
+ return JSON.stringify(value);
721
+ }
722
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
723
+ if (typeof value === "object" && value !== null) {
724
+ const input = value as Readonly<Record<string, unknown>>;
725
+ return `{${Object.keys(input).toSorted().map((key) =>
726
+ `${JSON.stringify(key)}:${canonicalJson(input[key])}`).join(",")}}`;
727
+ }
728
+ throw new TypeError("Canonical JSON accepts only JSON values.");
729
+ }
730
+
731
+ export function canonicalJsonBytes(value: unknown): Buffer {
732
+ return Buffer.from(`${canonicalJson(value)}\n`, "utf8");
733
+ }
734
+
735
+ function sha256(bytes: Uint8Array): string {
736
+ return createHash("sha256").update(bytes).digest("hex");
737
+ }
738
+
739
+ /** SHA-256 of the exact canonical immutable build.json bytes. */
740
+ export function kbEvidenceRoutingBuildContractSha256(configBytes: Uint8Array): string {
741
+ return sha256(configBytes);
742
+ }
743
+
744
+ /** Exact shared-spec commitment copied into each assessment and adjudication row. */
745
+ export function kbEvidenceRoutingPrivateQuestionSpecSha256(
746
+ spec: KbEvidenceRoutingPrivateQuestionSpec,
747
+ ): string {
748
+ const checked = parseSchema(
749
+ kbEvidenceRoutingPrivateQuestionSpecSchema,
750
+ spec,
751
+ "private question specification",
752
+ );
753
+ return sha256(canonicalJsonBytes(checked));
754
+ }
755
+
756
+ /**
757
+ * Commits the exact shared pre-assessment artifact, including reviewed document
758
+ * family assignments and the immutable build contract.
759
+ */
760
+ export function kbEvidenceRoutingPrivateQuestionSpecShardSha256(
761
+ shard: Extract<KbEvidenceRoutingPrivateAuthoringShard, { readonly kind: "question-specs" }>,
762
+ ): string {
763
+ const checked = parseSchema(
764
+ kbEvidenceRoutingPrivateQuestionSpecShardSchema,
765
+ shard,
766
+ "private question-spec shard",
767
+ );
768
+ return sha256(canonicalJsonBytes(checked));
769
+ }
770
+
771
+ /** Digest an exact, canonically ordered implementation source manifest. */
772
+ export function kbEvidenceRoutingImplementationSha256(
773
+ sources: readonly Readonly<{ readonly sourcePath: string; readonly bytes: Uint8Array }>[],
774
+ ): string {
775
+ return evaluationImplementationArtifactSha256V2(sources);
776
+ }
777
+
778
+ function gitResultBytes(result: Extract<GitCommandResult, { readonly status: "ok" }>): Buffer {
779
+ return typeof result.stdout === "string" ? Buffer.from(result.stdout, "utf8") : Buffer.from(result.stdout);
780
+ }
781
+
782
+ async function requiredGitOutput(
783
+ runGit: GitCommandProvider,
784
+ repositoryRoot: string,
785
+ arguments_: readonly string[],
786
+ maximumBytes: number,
787
+ label: string,
788
+ ): Promise<Buffer> {
789
+ const result = await runGit({
790
+ arguments: arguments_,
791
+ cwd: repositoryRoot,
792
+ timeoutMs: FROZEN_GIT_TIMEOUT_MS,
793
+ maxOutputBytes: maximumBytes,
794
+ });
795
+ if (result.status !== "ok") {
796
+ throw new Error(`${label} could not be verified: ${result.message}`);
797
+ }
798
+ return gitResultBytes(result);
799
+ }
800
+
801
+ /**
802
+ * Proves that every authoring source byte came from the digest-covered Git snapshot.
803
+ * Authoring shards and generated outputs may live in later commits; evidence may not.
804
+ */
805
+ export async function verifyKbEvidenceRoutingFrozenSources(
806
+ loaded: LoadedKbEvidenceRoutingAuthoring,
807
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
808
+ ): Promise<void> {
809
+ const runGit = dependencies.runGit ?? runGitCommand;
810
+ const { frozen } = loaded.config;
811
+ const resolvedCommit = (await requiredGitOutput(
812
+ runGit,
813
+ loaded.repositoryRoot,
814
+ ["rev-parse", "--verify", `${frozen.repositoryCommit}^{commit}`],
815
+ FROZEN_GIT_METADATA_BYTES,
816
+ "Frozen repository commit",
817
+ )).toString("utf8").trim();
818
+ if (resolvedCommit !== frozen.repositoryCommit) {
819
+ throw new Error(
820
+ `Frozen repository commit resolved to ${resolvedCommit || "an empty value"}, expected ${frozen.repositoryCommit}.`,
821
+ );
822
+ }
823
+
824
+ const vaultRevision = frozen.vaultRoot === "."
825
+ ? `${frozen.repositoryCommit}^{tree}`
826
+ : `${frozen.repositoryCommit}:${frozen.vaultRoot}`;
827
+ const resolvedTree = (await requiredGitOutput(
828
+ runGit,
829
+ loaded.repositoryRoot,
830
+ ["rev-parse", "--verify", vaultRevision],
831
+ FROZEN_GIT_METADATA_BYTES,
832
+ "Frozen vault tree",
833
+ )).toString("utf8").trim();
834
+ if (resolvedTree !== frozen.vaultTree) {
835
+ throw new Error(
836
+ `Frozen vault tree resolved to ${resolvedTree || "an empty value"}, expected ${frozen.vaultTree}.`,
837
+ );
838
+ }
839
+ const objectType = (await requiredGitOutput(
840
+ runGit,
841
+ loaded.repositoryRoot,
842
+ ["cat-file", "-t", vaultRevision],
843
+ FROZEN_GIT_METADATA_BYTES,
844
+ "Frozen vault object type",
845
+ )).toString("utf8").trim();
846
+ if (objectType !== "tree") throw new Error(`Frozen vault object must be a tree, received ${JSON.stringify(objectType)}.`);
847
+
848
+ for (const source of loaded.sourceFiles) {
849
+ const repositoryPath = frozen.vaultRoot === "."
850
+ ? source.sourcePath
851
+ : `${frozen.vaultRoot}/${source.sourcePath}`;
852
+ const frozenBytes = await requiredGitOutput(
853
+ runGit,
854
+ loaded.repositoryRoot,
855
+ ["show", `${frozen.repositoryCommit}:${repositoryPath}`],
856
+ Math.min(
857
+ MAX_EVALUATION_EVIDENCE_DOCUMENT_BYTES + 1,
858
+ Math.max(source.bytes.byteLength + 1, 1),
859
+ ),
860
+ `Frozen Markdown ${source.sourcePath}`,
861
+ );
862
+ if (!frozenBytes.equals(source.bytes)) {
863
+ throw new Error(`Frozen Markdown ${source.sourcePath} differs from the declared Git snapshot.`);
864
+ }
865
+ }
866
+
867
+ const descriptorById = new Map(loaded.config.retrievers.map((descriptor) => [descriptor.id, descriptor]));
868
+ const bindingsByRetriever = new Map<string, typeof loaded.config.implementationSources[number]>();
869
+ for (const [bindingIndex, binding] of loaded.config.implementationSources.entries()) {
870
+ if (!descriptorById.has(binding.retrieverId)) {
871
+ throw new Error(`implementationSources[${bindingIndex}] names unknown retriever ${binding.retrieverId}.`);
872
+ }
873
+ if (bindingsByRetriever.has(binding.retrieverId)) {
874
+ throw new Error(`implementationSources repeats retriever ${binding.retrieverId}.`);
875
+ }
876
+ const sourcePaths = binding.sourcePaths.map((sourcePath, sourceIndex) =>
877
+ confinedRelativePath(sourcePath, `implementationSources[${bindingIndex}].sourcePaths[${sourceIndex}]`));
878
+ const sortedPaths = sourcePaths.toSorted();
879
+ if (sourcePaths.some((sourcePath, sourceIndex) => sourcePath !== sortedPaths[sourceIndex])) {
880
+ throw new Error(`implementationSources[${bindingIndex}].sourcePaths must be in canonical order.`);
881
+ }
882
+ assertNoDuplicates(sourcePaths, `implementationSources[${bindingIndex}].sourcePaths`);
883
+ bindingsByRetriever.set(binding.retrieverId, binding);
884
+ }
885
+ const missingBindings = [...descriptorById.keys()]
886
+ .filter((retrieverId) => !bindingsByRetriever.has(retrieverId))
887
+ .toSorted();
888
+ if (missingBindings.length > 0) {
889
+ throw new Error(`Implementation sources are missing for retrievers: ${missingBindings.join(", ")}.`);
890
+ }
891
+
892
+ for (const retrieverId of [...descriptorById.keys()].toSorted()) {
893
+ const descriptor = descriptorById.get(retrieverId);
894
+ const binding = bindingsByRetriever.get(retrieverId);
895
+ if (descriptor === undefined || binding === undefined) throw new Error(`Lost retriever binding ${retrieverId}.`);
896
+ const sources = [];
897
+ for (const sourcePath of binding.sourcePaths) {
898
+ const bytes = await requiredGitOutput(
899
+ runGit,
900
+ loaded.repositoryRoot,
901
+ ["show", `${frozen.repositoryCommit}:${sourcePath}`],
902
+ MAX_IMPLEMENTATION_SOURCE_BYTES,
903
+ `Frozen implementation source ${sourcePath}`,
904
+ );
905
+ sources.push({ sourcePath, bytes });
906
+ }
907
+ const actual = kbEvidenceRoutingImplementationSha256(sources);
908
+ if (actual !== descriptor.implementationSha256) {
909
+ throw new Error(
910
+ `Retriever ${retrieverId} implementation digest ${descriptor.implementationSha256} does not match frozen source digest ${actual}.`,
911
+ );
912
+ }
913
+ }
914
+ }
915
+
916
+ function confinedRelativePath(value: string, label: string): string {
917
+ if (
918
+ value === ""
919
+ || value.normalize("NFC") !== value
920
+ || /[\0\r\n\\]/u.test(value)
921
+ || value.startsWith("/")
922
+ || value.startsWith("./")
923
+ || /^[a-z]:[\\/]/iu.test(value)
924
+ || value.split("/").some((part) => part === "" || part === "." || part === "..")
925
+ ) {
926
+ throw new Error(`${label} must be a canonical confined repository-relative path.`);
927
+ }
928
+ return value;
929
+ }
930
+
931
+ function identity(stat: {
932
+ readonly dev: number;
933
+ readonly ino: number;
934
+ readonly size: number;
935
+ readonly mtimeMs: number;
936
+ readonly ctimeMs: number;
937
+ readonly nlink: number;
938
+ }): FileIdentity {
939
+ return {
940
+ dev: stat.dev,
941
+ ino: stat.ino,
942
+ size: stat.size,
943
+ mtimeMs: stat.mtimeMs,
944
+ ctimeMs: stat.ctimeMs,
945
+ nlink: stat.nlink,
946
+ };
947
+ }
948
+
949
+ function sameIdentity(left: FileIdentity, right: FileIdentity): boolean {
950
+ return left.dev === right.dev
951
+ && left.ino === right.ino
952
+ && left.size === right.size
953
+ && left.mtimeMs === right.mtimeMs
954
+ && left.ctimeMs === right.ctimeMs
955
+ && left.nlink === right.nlink;
956
+ }
957
+
958
+ async function readBounded(handle: FileHandle, maximumBytes: number, label: string): Promise<Buffer> {
959
+ const chunks: Buffer[] = [];
960
+ let total = 0;
961
+ while (true) {
962
+ const remaining = maximumBytes + 1 - total;
963
+ if (remaining <= 0) throw new Error(`${label} exceeds ${maximumBytes} bytes.`);
964
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1_024, remaining));
965
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, null);
966
+ if (bytesRead === 0) break;
967
+ chunks.push(buffer.subarray(0, bytesRead));
968
+ total += bytesRead;
969
+ }
970
+ return Buffer.concat(chunks, total);
971
+ }
972
+
973
+ async function assertRepositoryComponents(
974
+ root: string,
975
+ relativePath: string,
976
+ finalKind: "file" | "directory" | "optional-file",
977
+ ): Promise<void> {
978
+ const parts = relativePath.split("/");
979
+ let current = root;
980
+ for (let index = 0; index < parts.length; index += 1) {
981
+ current = resolve(current, parts[index] ?? "");
982
+ const final = index === parts.length - 1;
983
+ let stat;
984
+ try {
985
+ stat = await lstat(current);
986
+ } catch (error) {
987
+ if (
988
+ final
989
+ && finalKind === "optional-file"
990
+ && typeof error === "object"
991
+ && error !== null
992
+ && "code" in error
993
+ && error.code === "ENOENT"
994
+ ) return;
995
+ throw error;
996
+ }
997
+ if (stat.isSymbolicLink()) throw new Error(`Path ${relativePath} traverses symbolic link ${current}.`);
998
+ if (!final && !stat.isDirectory()) {
999
+ throw new Error(`Path ${relativePath} traverses non-directory ${current}.`);
1000
+ }
1001
+ if (final && finalKind === "directory" && !stat.isDirectory()) {
1002
+ throw new Error(`Path ${relativePath} must resolve to a directory.`);
1003
+ }
1004
+ if (final && finalKind !== "directory" && !stat.isFile()) {
1005
+ throw new Error(`Path ${relativePath} must resolve to a regular file.`);
1006
+ }
1007
+ if (final && finalKind !== "directory" && stat.nlink !== 1) {
1008
+ throw new Error(`Path ${relativePath} must not be hard-linked (nlink=${stat.nlink}).`);
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ async function canonicalRepositoryRoot(input: string, configPath: string): Promise<string> {
1014
+ if (input.trim() === "" || /[\0\r\n]/u.test(input)) {
1015
+ throw new Error("repositoryRoot must be a non-empty local path.");
1016
+ }
1017
+ const lexical = isAbsolute(input) ? resolve(input) : resolve(dirname(configPath), input);
1018
+ const stat = await lstat(lexical);
1019
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
1020
+ throw new Error("repositoryRoot must name a real directory, not a symbolic link.");
1021
+ }
1022
+ return realpath(lexical);
1023
+ }
1024
+
1025
+ function pathContains(parent: string, child: string): boolean {
1026
+ const path = relative(parent, child);
1027
+ return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path));
1028
+ }
1029
+
1030
+ async function canonicalArtifactRoot(input: string | undefined, repositoryRoot: string): Promise<string> {
1031
+ if (input === undefined || input.trim() === "" || /[\0\r\n]/u.test(input)) {
1032
+ throw new Error("A separate artifact-B root is required.");
1033
+ }
1034
+ const lexical = resolve(input);
1035
+ const stat = await lstat(lexical);
1036
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
1037
+ throw new Error("artifact-B root must name a real directory, not a symbolic link.");
1038
+ }
1039
+ const canonical = await realpath(lexical);
1040
+ if (pathContains(repositoryRoot, canonical) || pathContains(canonical, repositoryRoot)) {
1041
+ throw new Error("artifact-B root must be disjoint from runtime repository A.");
1042
+ }
1043
+ return canonical;
1044
+ }
1045
+
1046
+ async function secureReadAbsoluteFile(
1047
+ absolutePath: string,
1048
+ maximumBytes: number,
1049
+ label: string,
1050
+ options: Readonly<{ readonly requireReadOnly?: boolean }> = {},
1051
+ ): Promise<SecureFile> {
1052
+ const before = await lstat(absolutePath);
1053
+ if (before.isSymbolicLink() || !before.isFile()) throw new Error(`${label} must be a regular non-symlink file.`);
1054
+ if (before.nlink !== 1) throw new Error(`${label} must not be hard-linked (nlink=${before.nlink}).`);
1055
+ if (options.requireReadOnly === true && (before.mode & 0o222) !== 0) {
1056
+ throw new Error(`${label} must be read-only before it is accepted.`);
1057
+ }
1058
+ if (before.size > maximumBytes) throw new Error(`${label} exceeds ${maximumBytes} bytes.`);
1059
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0);
1060
+ const handle = await open(absolutePath, flags);
1061
+ try {
1062
+ const opened = await handle.stat();
1063
+ if (!opened.isFile() || opened.nlink !== 1) throw new Error(`${label} changed before it was opened safely.`);
1064
+ if (options.requireReadOnly === true && (opened.mode & 0o222) !== 0) {
1065
+ throw new Error(`${label} became writable while it was opened.`);
1066
+ }
1067
+ const beforeIdentity = identity(opened);
1068
+ if (!sameIdentity(identity(before), beforeIdentity)) throw new Error(`${label} changed while it was opened.`);
1069
+ const bytes = await readBounded(handle, maximumBytes, label);
1070
+ const after = await handle.stat();
1071
+ if (!sameIdentity(beforeIdentity, identity(after)) || after.size !== bytes.byteLength) {
1072
+ throw new Error(`${label} changed while it was read.`);
1073
+ }
1074
+ if (options.requireReadOnly === true && (after.mode & 0o222) !== 0) {
1075
+ throw new Error(`${label} became writable while it was read.`);
1076
+ }
1077
+ return { absolutePath, bytes, sha256: sha256(bytes) };
1078
+ } finally {
1079
+ await handle.close();
1080
+ }
1081
+ }
1082
+
1083
+ async function secureReadRepositoryFile(
1084
+ root: string,
1085
+ relativePathInput: string,
1086
+ maximumBytes: number,
1087
+ label: string,
1088
+ ): Promise<SecureFile> {
1089
+ const relativePath = confinedRelativePath(relativePathInput, label);
1090
+ await assertRepositoryComponents(root, relativePath, "file");
1091
+ const absolutePath = resolve(root, ...relativePath.split("/"));
1092
+ const canonical = await realpath(absolutePath);
1093
+ if (canonical !== absolutePath) throw new Error(`${label} resolves through a symbolic link.`);
1094
+ return {
1095
+ ...await secureReadAbsoluteFile(absolutePath, maximumBytes, label),
1096
+ relativePath,
1097
+ };
1098
+ }
1099
+
1100
+ async function validateOutputPath(root: string, input: string, label: string): Promise<string> {
1101
+ const relativePath = confinedRelativePath(input, label);
1102
+ const parentRelative = dirname(relativePath).split(sep).join("/");
1103
+ if (parentRelative === ".") {
1104
+ // The repository root itself has already been checked.
1105
+ } else {
1106
+ await assertRepositoryComponents(root, parentRelative, "directory");
1107
+ }
1108
+ await assertRepositoryComponents(root, relativePath, "optional-file");
1109
+ return resolve(root, ...relativePath.split("/"));
1110
+ }
1111
+
1112
+ function requireAuthoringPath(path: string, label: string): string {
1113
+ const canonical = confinedRelativePath(path, label);
1114
+ if (!canonical.startsWith(`${AUTHORING_DIRECTORY}/`) || !canonical.endsWith(".json")) {
1115
+ throw new Error(`${label} must be a JSON file under ${AUTHORING_DIRECTORY}/.`);
1116
+ }
1117
+ return canonical;
1118
+ }
1119
+
1120
+ function requireHeldOutPath(path: string, label: string): string {
1121
+ const canonical = confinedRelativePath(path, label);
1122
+ if (!canonical.startsWith("held-out/") || !canonical.endsWith(".json")) {
1123
+ throw new Error(`${label} must be a JSON file under artifact-B held-out/.`);
1124
+ }
1125
+ return canonical;
1126
+ }
1127
+
1128
+ function duplicates(values: readonly string[]): readonly string[] {
1129
+ const seen = new Set<string>();
1130
+ const repeated = new Set<string>();
1131
+ for (const value of values) {
1132
+ if (seen.has(value)) repeated.add(value);
1133
+ seen.add(value);
1134
+ }
1135
+ return [...repeated].toSorted();
1136
+ }
1137
+
1138
+ function assertNoDuplicates(values: readonly string[], label: string): void {
1139
+ const repeated = duplicates(values);
1140
+ if (repeated.length > 0) throw new Error(`${label} contains duplicates: ${repeated.join(", ")}.`);
1141
+ }
1142
+
1143
+ function normalizedPrompt(text: string): string {
1144
+ return text
1145
+ .normalize("NFKC")
1146
+ .toLowerCase()
1147
+ .replace(/[\p{P}\p{S}]+/gu, " ")
1148
+ .replace(/\s+/gu, " ")
1149
+ .trim();
1150
+ }
1151
+
1152
+ function promptNgrams(text: string, size: number): ReadonlySet<string> {
1153
+ const tokens = normalizedPrompt(text).split(" ").filter((token) => token !== "");
1154
+ if (tokens.length === 0) return new Set();
1155
+ if (tokens.length < size) return new Set([tokens.join(" ")]);
1156
+ const grams = new Set<string>();
1157
+ for (let index = 0; index <= tokens.length - size; index += 1) {
1158
+ grams.add(tokens.slice(index, index + size).join(" "));
1159
+ }
1160
+ return grams;
1161
+ }
1162
+
1163
+ function jaccard(left: ReadonlySet<string>, right: ReadonlySet<string>): number {
1164
+ if (left.size === 0 || right.size === 0) return 0;
1165
+ let intersection = 0;
1166
+ for (const item of left) if (right.has(item)) intersection += 1;
1167
+ return intersection / (left.size + right.size - intersection);
1168
+ }
1169
+
1170
+ function questionSourcePaths(
1171
+ question: KbEvidenceRoutingAuthoringShard["questions"][number],
1172
+ ): readonly string[] {
1173
+ return [...new Set([
1174
+ ...question.gold.documents.map(({ sourcePath }) => sourcePath),
1175
+ ...question.gold.evidenceUnits.map(({ selector }) => selector.sourcePath),
1176
+ ...question.gold.nuggets.flatMap(({ acceptableSupportSets }) =>
1177
+ acceptableSupportSets.flatMap(({ evidence }) => evidence.map(({ sourcePath }) => sourcePath))),
1178
+ ...question.rawAssessments.flatMap(({ documents }) =>
1179
+ documents.map(({ sourcePath }) => sourcePath)),
1180
+ ...question.rawAssessments.flatMap(({ evidenceUnits }) =>
1181
+ evidenceUnits.map(({ selector }) => selector.sourcePath)),
1182
+ ])].toSorted();
1183
+ }
1184
+
1185
+ function promotionQuestion(
1186
+ question: KbEvidenceRoutingAuthoringShard["questions"][number],
1187
+ ): HumanAuthoredEvaluationQuestionV2 {
1188
+ const { negativeSubtype, ...required } = question;
1189
+ const inputs = {
1190
+ ...(question.inputs.text === undefined ? {} : { text: question.inputs.text }),
1191
+ ...(question.inputs.noteId === undefined ? {} : { noteId: question.inputs.noteId }),
1192
+ ...(question.inputs.metadata === undefined ? {} : { metadata: question.inputs.metadata }),
1193
+ ...(question.inputs.graph === undefined ? {} : { graph: question.inputs.graph }),
1194
+ ...(question.inputs.context === undefined ? {} : { context: question.inputs.context }),
1195
+ ...(question.inputs.history === undefined ? {} : { history: question.inputs.history }),
1196
+ };
1197
+ const normalized = { ...required, inputs };
1198
+ const result = negativeSubtype === undefined
1199
+ ? normalized
1200
+ : { ...normalized, negativeSubtype };
1201
+ return result as unknown as HumanAuthoredEvaluationQuestionV2;
1202
+ }
1203
+
1204
+ function privateJoinKey(questionKey: string, assessorId: string): string {
1205
+ return canonicalJson([questionKey, assessorId]);
1206
+ }
1207
+
1208
+ function joinPrivateAuthoringShards(
1209
+ shards: readonly KbEvidenceRoutingPrivateAuthoringShard[],
1210
+ expectedBuildContractSha256: string,
1211
+ declaredAssessorIds: readonly string[],
1212
+ declaredSourceFamilyReviewerIds: readonly string[],
1213
+ ): readonly KbEvidenceRoutingAuthoringShard[] {
1214
+ if (shards.length === 0) return Object.freeze([]);
1215
+
1216
+ const errors: string[] = [];
1217
+ const declaredAssessors = new Set(declaredAssessorIds);
1218
+ const declaredSourceFamilyReviewers = new Set(declaredSourceFamilyReviewerIds);
1219
+ const documents: KbEvidenceRoutingAuthoringShard["documents"][number][] = [];
1220
+ const specifications = new Map<string, KbEvidenceRoutingPrivateQuestionSpec>();
1221
+ const specificationShardSha256ByQuestionKey = new Map<string, string>();
1222
+ const questionSpecShardSha256ByFamilyKey = new Map<string, string>();
1223
+ const questionSpecShardSha256BySourcePath = new Map<string, string>();
1224
+ const judgments = new Map<
1225
+ string,
1226
+ Readonly<{
1227
+ assessorId: string;
1228
+ judgment: Extract<
1229
+ KbEvidenceRoutingPrivateAuthoringShard,
1230
+ { readonly kind: "assessor-judgments" }
1231
+ >["judgments"][number];
1232
+ }>
1233
+ >();
1234
+ const adjudications = new Map<
1235
+ string,
1236
+ Extract<KbEvidenceRoutingPrivateAuthoringShard, { readonly kind: "adjudications" }>[
1237
+ "adjudications"
1238
+ ][number]
1239
+ >();
1240
+
1241
+ for (const [shardIndex, shard] of shards.entries()) {
1242
+ if (shard.buildContractSha256 !== expectedBuildContractSha256) {
1243
+ errors.push(
1244
+ `Private shard ${shardIndex} (${shard.kind}) buildContractSha256 ${shard.buildContractSha256} does not match exact build config ${expectedBuildContractSha256}.`,
1245
+ );
1246
+ }
1247
+ if (shard.kind === "question-specs") {
1248
+ const questionSpecShardSha256 = kbEvidenceRoutingPrivateQuestionSpecShardSha256(shard);
1249
+ documents.push(...shard.documents);
1250
+ for (const document of shard.documents) {
1251
+ const familyOwner = questionSpecShardSha256ByFamilyKey.get(document.sourceFamilyKey);
1252
+ if (familyOwner !== undefined && familyOwner !== questionSpecShardSha256) {
1253
+ errors.push(
1254
+ `Private source family ${document.sourceFamilyKey} spans more than one question-spec shard.`,
1255
+ );
1256
+ } else {
1257
+ questionSpecShardSha256ByFamilyKey.set(
1258
+ document.sourceFamilyKey,
1259
+ questionSpecShardSha256,
1260
+ );
1261
+ }
1262
+ const sourceOwner = questionSpecShardSha256BySourcePath.get(document.sourcePath);
1263
+ if (sourceOwner !== undefined && sourceOwner !== questionSpecShardSha256) {
1264
+ errors.push(
1265
+ `Private source path ${document.sourcePath} spans more than one question-spec shard.`,
1266
+ );
1267
+ } else {
1268
+ questionSpecShardSha256BySourcePath.set(document.sourcePath, questionSpecShardSha256);
1269
+ }
1270
+ const undeclaredReviewers = document.sourceFamilyReviewerIds
1271
+ .filter((reviewerId) => !declaredSourceFamilyReviewers.has(reviewerId));
1272
+ if (undeclaredReviewers.length > 0) {
1273
+ errors.push(
1274
+ `Private source family ${document.sourceFamilyKey} names undeclared assignment reviewers: ${undeclaredReviewers.toSorted().join(", ")}.`,
1275
+ );
1276
+ }
1277
+ }
1278
+ for (const specification of shard.questions) {
1279
+ if (specifications.has(specification.key)) {
1280
+ errors.push(`Private question specification ${specification.key} is ambiguous because it appears more than once.`);
1281
+ continue;
1282
+ }
1283
+ specifications.set(specification.key, specification);
1284
+ specificationShardSha256ByQuestionKey.set(specification.key, questionSpecShardSha256);
1285
+ const duplicateAssessorIds = duplicates(specification.assignedAssessorIds);
1286
+ if (duplicateAssessorIds.length > 0) {
1287
+ errors.push(
1288
+ `Private question ${specification.key} repeats assigned assessors: ${duplicateAssessorIds.join(", ")}.`,
1289
+ );
1290
+ }
1291
+ const canonicalAssessorIds = specification.assignedAssessorIds.toSorted();
1292
+ if (specification.assignedAssessorIds.some((assessorId, index) =>
1293
+ assessorId !== canonicalAssessorIds[index])) {
1294
+ errors.push(`Private question ${specification.key} assignedAssessorIds must be in canonical order.`);
1295
+ }
1296
+ const undeclared = specification.assignedAssessorIds
1297
+ .filter((assessorId) => !declaredAssessors.has(assessorId))
1298
+ .toSorted();
1299
+ if (undeclared.length > 0) {
1300
+ errors.push(`Private question ${specification.key} assigns undeclared assessors: ${undeclared.join(", ")}.`);
1301
+ }
1302
+ }
1303
+ continue;
1304
+ }
1305
+ if (shard.kind === "assessor-judgments") {
1306
+ if (!declaredAssessors.has(shard.assessorId)) {
1307
+ errors.push(`Private assessor-judgment shard names undeclared assessor ${shard.assessorId}.`);
1308
+ }
1309
+ for (const judgment of shard.judgments) {
1310
+ const key = privateJoinKey(judgment.questionKey, shard.assessorId);
1311
+ if (judgments.has(key)) {
1312
+ errors.push(
1313
+ `Private judgment (${judgment.questionKey}, ${shard.assessorId}) appears more than once.`,
1314
+ );
1315
+ continue;
1316
+ }
1317
+ judgments.set(key, Object.freeze({ assessorId: shard.assessorId, judgment }));
1318
+ }
1319
+ continue;
1320
+ }
1321
+ for (const adjudication of shard.adjudications) {
1322
+ if (adjudications.has(adjudication.questionKey)) {
1323
+ errors.push(`Private question ${adjudication.questionKey} has more than one adjudication.`);
1324
+ continue;
1325
+ }
1326
+ adjudications.set(adjudication.questionKey, adjudication);
1327
+ }
1328
+ }
1329
+
1330
+ for (const { assessorId, judgment } of judgments.values()) {
1331
+ const { questionKey } = judgment;
1332
+ const specification = specifications.get(questionKey);
1333
+ if (specification === undefined) {
1334
+ errors.push(`Private judgment (${questionKey}, ${assessorId}) has no shared question specification.`);
1335
+ continue;
1336
+ }
1337
+ if (!specification.assignedAssessorIds.includes(assessorId)) {
1338
+ errors.push(`Private judgment (${questionKey}, ${assessorId}) is extra; that assessor is not assigned.`);
1339
+ }
1340
+ const expectedSpecSha256 = kbEvidenceRoutingPrivateQuestionSpecSha256(specification);
1341
+ if (judgment.questionSpecSha256 !== expectedSpecSha256) {
1342
+ errors.push(
1343
+ `Private judgment (${questionKey}, ${assessorId}) questionSpecSha256 does not match the shared specification.`,
1344
+ );
1345
+ }
1346
+ const expectedShardSha256 = specificationShardSha256ByQuestionKey.get(questionKey);
1347
+ if (judgment.questionSpecShardSha256 !== expectedShardSha256) {
1348
+ errors.push(
1349
+ `Private judgment (${questionKey}, ${assessorId}) questionSpecShardSha256 does not match the shared question-spec shard.`,
1350
+ );
1351
+ }
1352
+ }
1353
+
1354
+ for (const [questionKey, adjudication] of adjudications) {
1355
+ const specification = specifications.get(questionKey);
1356
+ if (specification === undefined) {
1357
+ errors.push(`Private adjudication ${questionKey} has no shared question specification.`);
1358
+ continue;
1359
+ }
1360
+ const expectedSpecSha256 = kbEvidenceRoutingPrivateQuestionSpecSha256(specification);
1361
+ if (adjudication.questionSpecSha256 !== expectedSpecSha256) {
1362
+ errors.push(`Private adjudication ${questionKey} questionSpecSha256 does not match the shared specification.`);
1363
+ }
1364
+ const expectedShardSha256 = specificationShardSha256ByQuestionKey.get(questionKey);
1365
+ if (adjudication.questionSpecShardSha256 !== expectedShardSha256) {
1366
+ errors.push(
1367
+ `Private adjudication ${questionKey} questionSpecShardSha256 does not match the shared question-spec shard.`,
1368
+ );
1369
+ }
1370
+ }
1371
+
1372
+ for (const [questionKey, specification] of specifications) {
1373
+ for (const assessorId of specification.assignedAssessorIds) {
1374
+ if (!judgments.has(privateJoinKey(questionKey, assessorId))) {
1375
+ errors.push(`Private question ${questionKey} is missing judgment from assigned assessor ${assessorId}.`);
1376
+ }
1377
+ }
1378
+ if (!adjudications.has(questionKey)) {
1379
+ errors.push(`Private question ${questionKey} is missing its adjudication.`);
1380
+ }
1381
+ }
1382
+
1383
+ if (errors.length > 0) {
1384
+ throw new Error(`Private held-out join failed:\n${[...new Set(errors)].toSorted().join("\n")}`);
1385
+ }
1386
+
1387
+ const questions = [...specifications.values()]
1388
+ .toSorted((left, right) => left.key.localeCompare(right.key))
1389
+ .map((specification): KbEvidenceRoutingAuthoringShard["questions"][number] => {
1390
+ const { assignedAssessorIds, ...shared } = specification;
1391
+ const finalJudgment = adjudications.get(specification.key);
1392
+ if (finalJudgment === undefined) throw new Error(`Lost private adjudication ${specification.key}.`);
1393
+ const rawAssessments = assignedAssessorIds.map((assessorId) => {
1394
+ const binding = judgments.get(privateJoinKey(specification.key, assessorId));
1395
+ if (binding === undefined) {
1396
+ throw new Error(`Lost private judgment (${specification.key}, ${assessorId}).`);
1397
+ }
1398
+ const { judgment } = binding;
1399
+ return {
1400
+ assessorId,
1401
+ expectedSupport: judgment.expectedSupport,
1402
+ documents: judgment.documents,
1403
+ evidenceUnits: judgment.evidenceUnits,
1404
+ nuggets: judgment.nuggets,
1405
+ };
1406
+ });
1407
+ return {
1408
+ ...shared,
1409
+ expectedSupport: finalJudgment.expectedSupport,
1410
+ gold: finalJudgment.gold,
1411
+ rawAssessments,
1412
+ adjudication: finalJudgment.adjudication,
1413
+ };
1414
+ });
1415
+ for (const question of questions) {
1416
+ const questionShardSha256 = specificationShardSha256ByQuestionKey.get(question.key);
1417
+ if (questionShardSha256 === undefined) continue;
1418
+ for (const sourcePath of questionSourcePaths(question)) {
1419
+ const sourceShardSha256 = questionSpecShardSha256BySourcePath.get(sourcePath);
1420
+ if (sourceShardSha256 !== undefined && sourceShardSha256 !== questionShardSha256) {
1421
+ errors.push(
1422
+ `Private question ${question.key} references source ${sourcePath} owned by a different question-spec shard.`,
1423
+ );
1424
+ }
1425
+ }
1426
+ }
1427
+ if (errors.length > 0) {
1428
+ throw new Error(`Private held-out join failed:\n${[...new Set(errors)].toSorted().join("\n")}`);
1429
+ }
1430
+ return Object.freeze([{
1431
+ documents: documents.toSorted((left, right) => left.sourcePath.localeCompare(right.sourcePath)),
1432
+ questions,
1433
+ }]);
1434
+ }
1435
+
1436
+ function assertExplicitPromotionQuestionDocuments(
1437
+ questions: readonly KbEvidenceRoutingAuthoringShard["questions"][number][],
1438
+ metadataByPath: ReadonlyMap<string, KbEvidenceRoutingAuthoringShard["documents"][number]>,
1439
+ ): void {
1440
+ const questionKeysByPath = new Map<string, Set<string>>();
1441
+ for (const question of questions) {
1442
+ for (const sourcePathInput of questionSourcePaths(question)) {
1443
+ const sourcePath = confinedRelativePath(
1444
+ sourcePathInput,
1445
+ `promotion question ${question.key} source path`,
1446
+ );
1447
+ const questionKeys = questionKeysByPath.get(sourcePath) ?? new Set<string>();
1448
+ questionKeys.add(question.key);
1449
+ questionKeysByPath.set(sourcePath, questionKeys);
1450
+ }
1451
+ }
1452
+ const missing = [...questionKeysByPath]
1453
+ .filter(([sourcePath]) => !metadataByPath.has(sourcePath))
1454
+ .map(([sourcePath, questionKeys]) => `${sourcePath} (${[...questionKeys].toSorted().join(", ")})`)
1455
+ .toSorted();
1456
+ const reserved = [...questionKeysByPath.keys()]
1457
+ .filter((sourcePath) => metadataByPath.get(sourcePath)?.sourceFamilyKey.startsWith("catalog:"))
1458
+ .toSorted();
1459
+ if (missing.length === 0 && reserved.length === 0) return;
1460
+ throw new Error([
1461
+ "Promotion question sources require explicit reviewed document metadata and sourceFamilyKey; inferred catalog metadata is catalog-only.",
1462
+ ...(missing.length === 0 ? [] : [`Missing reviewed documents: ${missing.join("; ")}.`]),
1463
+ ...(reserved.length === 0
1464
+ ? []
1465
+ : [`Reviewed promotion sources must not use the reserved catalog: family prefix: ${reserved.join(", ")}.`]),
1466
+ ].join("\n"));
1467
+ }
1468
+
1469
+ type IsolationPartition = Readonly<{
1470
+ readonly questions: readonly KbEvidenceRoutingAuthoringShard["questions"][number][];
1471
+ readonly sourcePaths: ReadonlySet<string>;
1472
+ readonly sourceFamilies: ReadonlySet<string>;
1473
+ }>;
1474
+
1475
+ function isolationPartition(
1476
+ shards: readonly KbEvidenceRoutingAuthoringShard[],
1477
+ label: string,
1478
+ ): IsolationPartition {
1479
+ const questions = shards.flatMap(({ questions: shardQuestions }) => shardQuestions);
1480
+ const familyByPath = new Map<string, string>();
1481
+ for (const [documentIndex, document] of shards.flatMap(({ documents }) => documents).entries()) {
1482
+ const sourcePath = confinedRelativePath(
1483
+ document.sourcePath,
1484
+ `${label}.documents[${documentIndex}].sourcePath`,
1485
+ );
1486
+ const previous = familyByPath.get(sourcePath);
1487
+ if (previous !== undefined && previous !== document.sourceFamilyKey) {
1488
+ throw new Error(
1489
+ `${label} assigns source path ${sourcePath} to conflicting source families ${previous} and ${document.sourceFamilyKey}.`,
1490
+ );
1491
+ }
1492
+ familyByPath.set(sourcePath, document.sourceFamilyKey);
1493
+ }
1494
+ const sourcePaths = new Set(familyByPath.keys());
1495
+ for (const question of questions) {
1496
+ for (const sourcePathInput of questionSourcePaths(question)) {
1497
+ sourcePaths.add(confinedRelativePath(
1498
+ sourcePathInput,
1499
+ `${label} question ${question.key} source path`,
1500
+ ));
1501
+ }
1502
+ }
1503
+ const sourceFamilies = new Set([...sourcePaths].map((sourcePath) =>
1504
+ familyByPath.get(sourcePath) ?? `catalog:${sourcePath}`));
1505
+ return Object.freeze({
1506
+ questions: Object.freeze(questions),
1507
+ sourcePaths,
1508
+ sourceFamilies,
1509
+ });
1510
+ }
1511
+
1512
+ function intersections(left: ReadonlySet<string>, right: ReadonlySet<string>): readonly string[] {
1513
+ return [...left].filter((value) => right.has(value)).toSorted();
1514
+ }
1515
+
1516
+ /**
1517
+ * Keep every visible engineering input disjoint from the private promotion set.
1518
+ * QA participates in this review boundary but is intentionally absent from the
1519
+ * compiled/scored corpus assembled below.
1520
+ */
1521
+ function assertVisibleHeldOutIsolation(options: Readonly<{
1522
+ readonly development: readonly KbEvidenceRoutingAuthoringShard[];
1523
+ readonly qa: readonly KbEvidenceRoutingAuthoringShard[];
1524
+ readonly heldOut: readonly KbEvidenceRoutingAuthoringShard[];
1525
+ readonly sourceSha256ByPath: ReadonlyMap<string, string>;
1526
+ readonly ngramSize: number;
1527
+ readonly crossSplitNgramThreshold: number;
1528
+ }>): void {
1529
+ const visible = isolationPartition([...options.development, ...options.qa], "visible authoring");
1530
+ const heldOut = isolationPartition(options.heldOut, "private held-out authoring");
1531
+ const errors: string[] = [];
1532
+
1533
+ const heldOutKeys = new Set(heldOut.questions.map(({ key }) => key));
1534
+ const repeatedKeys = [...new Set(visible.questions
1535
+ .map(({ key }) => key)
1536
+ .filter((key) => heldOutKeys.has(key)))].toSorted();
1537
+ if (repeatedKeys.length > 0) {
1538
+ errors.push(`Visible and private held-out question keys overlap: ${repeatedKeys.join(", ")}.`);
1539
+ }
1540
+
1541
+ const visibleNormalized = new Map<string, string[]>();
1542
+ for (const question of visible.questions) {
1543
+ const normalized = normalizedPrompt(question.text);
1544
+ const keys = visibleNormalized.get(normalized) ?? [];
1545
+ keys.push(question.key);
1546
+ visibleNormalized.set(normalized, keys);
1547
+ }
1548
+ const heldOutNormalized = new Map<string, string[]>();
1549
+ for (const question of heldOut.questions) {
1550
+ const normalized = normalizedPrompt(question.text);
1551
+ const keys = heldOutNormalized.get(normalized) ?? [];
1552
+ keys.push(question.key);
1553
+ heldOutNormalized.set(normalized, keys);
1554
+ }
1555
+ for (const normalized of [...visibleNormalized.keys()].filter((value) =>
1556
+ value !== "" && heldOutNormalized.has(value)).toSorted()) {
1557
+ errors.push(
1558
+ `Visible questions ${(visibleNormalized.get(normalized) ?? []).toSorted().join(", ")} and private held-out questions ${(heldOutNormalized.get(normalized) ?? []).toSorted().join(", ")} share one normalized prompt.`,
1559
+ );
1560
+ }
1561
+
1562
+ const visibleGrams = new Map(visible.questions.map((question) => [
1563
+ question,
1564
+ promptNgrams(question.text, options.ngramSize),
1565
+ ]));
1566
+ const heldOutGrams = new Map(heldOut.questions.map((question) => [
1567
+ question,
1568
+ promptNgrams(question.text, options.ngramSize),
1569
+ ]));
1570
+ for (const visibleQuestion of visible.questions) {
1571
+ for (const heldOutQuestion of heldOut.questions) {
1572
+ if (normalizedPrompt(visibleQuestion.text) === normalizedPrompt(heldOutQuestion.text)) continue;
1573
+ const overlap = jaccard(
1574
+ visibleGrams.get(visibleQuestion) ?? new Set(),
1575
+ heldOutGrams.get(heldOutQuestion) ?? new Set(),
1576
+ );
1577
+ if (overlap < options.crossSplitNgramThreshold) continue;
1578
+ errors.push(
1579
+ `Visible question ${visibleQuestion.key} and private held-out question ${heldOutQuestion.key} have ${overlap.toFixed(6)} ${options.ngramSize}-gram Jaccard overlap.`,
1580
+ );
1581
+ }
1582
+ }
1583
+
1584
+ const repeatedPaths = intersections(visible.sourcePaths, heldOut.sourcePaths);
1585
+ if (repeatedPaths.length > 0) {
1586
+ errors.push(`Visible and private held-out source paths overlap: ${repeatedPaths.join(", ")}.`);
1587
+ }
1588
+ const repeatedFamilies = intersections(visible.sourceFamilies, heldOut.sourceFamilies);
1589
+ if (repeatedFamilies.length > 0) {
1590
+ errors.push(`Visible and private held-out source families overlap: ${repeatedFamilies.join(", ")}.`);
1591
+ }
1592
+
1593
+ const visiblePathsBySha = new Map<string, string[]>();
1594
+ for (const sourcePath of visible.sourcePaths) {
1595
+ const digest = options.sourceSha256ByPath.get(sourcePath);
1596
+ if (digest === undefined) {
1597
+ errors.push(`Visible source path ${sourcePath} is absent from the frozen Markdown catalog.`);
1598
+ continue;
1599
+ }
1600
+ const paths = visiblePathsBySha.get(digest) ?? [];
1601
+ paths.push(sourcePath);
1602
+ visiblePathsBySha.set(digest, paths);
1603
+ }
1604
+ const heldOutPathsBySha = new Map<string, string[]>();
1605
+ for (const sourcePath of heldOut.sourcePaths) {
1606
+ const digest = options.sourceSha256ByPath.get(sourcePath);
1607
+ if (digest === undefined) {
1608
+ errors.push(`Private held-out source path ${sourcePath} is absent from the frozen Markdown catalog.`);
1609
+ continue;
1610
+ }
1611
+ const paths = heldOutPathsBySha.get(digest) ?? [];
1612
+ paths.push(sourcePath);
1613
+ heldOutPathsBySha.set(digest, paths);
1614
+ }
1615
+ for (const digest of [...visiblePathsBySha.keys()].filter((value) =>
1616
+ heldOutPathsBySha.has(value)).toSorted()) {
1617
+ errors.push(
1618
+ `Visible sources ${(visiblePathsBySha.get(digest) ?? []).toSorted().join(", ")} and private held-out sources ${(heldOutPathsBySha.get(digest) ?? []).toSorted().join(", ")} contain byte-identical frozen content (${digest}).`,
1619
+ );
1620
+ }
1621
+
1622
+ if (errors.length > 0) {
1623
+ throw new Error(`Visible/private held-out isolation failed:\n${errors.toSorted().join("\n")}`);
1624
+ }
1625
+ }
1626
+
1627
+ function compileOrThrow(
1628
+ input: PromotionCorpusAuthoringInputV2,
1629
+ ): Extract<PromotionCorpusCompilationResultV2, { readonly ok: true }> {
1630
+ const compilation = compileRetrievalEvaluationCorpusAuthoringV2(input);
1631
+ if (!compilation.ok) {
1632
+ throw new Error(compilation.errors.map(({ code, message }) => `${code}: ${message}`).join("\n"));
1633
+ }
1634
+ return compilation;
1635
+ }
1636
+
1637
+ export function assertKbEvidenceRoutingVisibleReviewReady(
1638
+ diagnostics: Extract<
1639
+ PromotionCorpusCompilationResultV2,
1640
+ { readonly ok: true }
1641
+ >["diagnostics"],
1642
+ ): void {
1643
+ const failedQuotas = diagnostics.quotaLedger
1644
+ .filter(({ met }) => !met)
1645
+ .map(({ id }) => id)
1646
+ .toSorted();
1647
+ if (failedQuotas.length > 0 || !diagnostics.labelPredictability.met) {
1648
+ throw new Error([
1649
+ "Visible development and QA review is not ready.",
1650
+ ...(failedQuotas.length === 0
1651
+ ? []
1652
+ : [`Failed exact quota or balance rows: ${failedQuotas.join(", ")}.`]),
1653
+ ...(diagnostics.labelPredictability.met
1654
+ ? []
1655
+ : ["Prompt labels exceed the sealed text-only predictability ceiling."]),
1656
+ "Visible QA paired-power diagnostics remain review-only and are not promotion evidence.",
1657
+ ].join("\n"));
1658
+ }
1659
+ }
1660
+
1661
+ function parseExternalSeal(input: unknown): Extract<
1662
+ EvaluationExternalCorpusSealV2,
1663
+ { readonly expectedCorpusSha256: string }
1664
+ > {
1665
+ return parseSchema(
1666
+ z.strictObject({ expectedCorpusSha256: sha256Schema }),
1667
+ input,
1668
+ "external seal",
1669
+ );
1670
+ }
1671
+
1672
+ async function loadConfig(configPathInput: string): Promise<{
1673
+ readonly configPath: string;
1674
+ readonly file: SecureFile;
1675
+ readonly config: KbEvidenceRoutingEvaluationBuildConfig;
1676
+ }> {
1677
+ if (configPathInput.trim() === "") throw new Error("An explicit config path is required.");
1678
+ const configPath = resolve(configPathInput);
1679
+ if (await realpath(configPath) !== configPath) {
1680
+ throw new Error("build config path must be canonical and must not traverse a symbolic link.");
1681
+ }
1682
+ const file = await secureReadAbsoluteFile(
1683
+ configPath,
1684
+ MAX_CONFIG_BYTES,
1685
+ "build config",
1686
+ { requireReadOnly: true },
1687
+ );
1688
+ if (await realpath(configPath) !== configPath) {
1689
+ throw new Error("build config path changed or began traversing a symbolic link while it was read.");
1690
+ }
1691
+ const config = parseSchema(
1692
+ kbEvidenceRoutingEvaluationBuildConfigSchema,
1693
+ parseJson(file.bytes, "build config"),
1694
+ "build config",
1695
+ );
1696
+ if (!file.bytes.equals(canonicalJsonBytes(config))) {
1697
+ throw new Error("build config must contain the exact canonical JSON bytes for its parsed value.");
1698
+ }
1699
+ return { configPath, file, config };
1700
+ }
1701
+
1702
+ async function frozenMarkdownPaths(
1703
+ config: KbEvidenceRoutingEvaluationBuildConfig,
1704
+ repositoryRoot: string,
1705
+ runGit: GitCommandProvider,
1706
+ ): Promise<readonly string[]> {
1707
+ const revision = config.frozen.vaultRoot === "."
1708
+ ? `${config.frozen.repositoryCommit}^{tree}`
1709
+ : `${config.frozen.repositoryCommit}:${config.frozen.vaultRoot}`;
1710
+ const bytes = await requiredGitOutput(
1711
+ runGit,
1712
+ repositoryRoot,
1713
+ ["ls-tree", "-r", "-z", "--name-only", revision],
1714
+ MAX_VAULT_LIST_BYTES,
1715
+ "Frozen vault Markdown catalog",
1716
+ );
1717
+ if (bytes.byteLength === 0 || bytes.at(-1) !== 0) {
1718
+ throw new Error("Frozen vault file catalog must be non-empty NUL-delimited Git output.");
1719
+ }
1720
+ const paths = bytes.subarray(0, -1).toString("utf8").split("\0")
1721
+ .map((path, index) => confinedRelativePath(path, `frozen vault entry ${index}`))
1722
+ .filter((path) => path.endsWith(".md"));
1723
+ assertNoDuplicates(paths, "Frozen vault Markdown paths");
1724
+ if (paths.length === 0 || paths.length > MAX_EVALUATION_EVIDENCE_DOCUMENTS) {
1725
+ throw new Error(
1726
+ `Frozen vault must contain from 1 through ${MAX_EVALUATION_EVIDENCE_DOCUMENTS} Markdown documents.`,
1727
+ );
1728
+ }
1729
+ return Object.freeze(paths.toSorted());
1730
+ }
1731
+
1732
+ function derivedDocumentMetadata(sourcePath: string): KbEvidenceRoutingAuthoringShard["documents"][number] {
1733
+ const capture = sourcePath.startsWith("sources/");
1734
+ return Object.freeze({
1735
+ sourcePath,
1736
+ sourceFamilyKey: `catalog:${sourcePath}`,
1737
+ sourceClass: capture ? "captured-source" as const : "authored-note" as const,
1738
+ trustClass: capture ? "untrusted-capture" as const : "maintained-synthesis" as const,
1739
+ });
1740
+ }
1741
+
1742
+ function partitionDocumentMetadata(
1743
+ shards: readonly KbEvidenceRoutingAuthoringShard[],
1744
+ label: string,
1745
+ ): ReadonlyMap<string, KbEvidenceRoutingAuthoringShard["documents"][number]> {
1746
+ const byPath = new Map<string, KbEvidenceRoutingAuthoringShard["documents"][number]>();
1747
+ for (const [index, metadata] of shards.flatMap(({ documents }) => documents).entries()) {
1748
+ const sourcePath = confinedRelativePath(metadata.sourcePath, `${label}.documents[${index}].sourcePath`);
1749
+ const previous = byPath.get(sourcePath);
1750
+ if (
1751
+ previous !== undefined
1752
+ && !canonicalJsonBytes(previous).equals(canonicalJsonBytes(metadata))
1753
+ ) throw new Error(`${label} gives source path ${sourcePath} conflicting document metadata.`);
1754
+ byPath.set(sourcePath, metadata);
1755
+ }
1756
+ return byPath;
1757
+ }
1758
+
1759
+ async function readAuthoringShards(
1760
+ root: string,
1761
+ paths: readonly string[],
1762
+ label: string,
1763
+ ): Promise<{ readonly files: readonly SecureFile[]; readonly shards: readonly KbEvidenceRoutingAuthoringShard[] }> {
1764
+ let aggregateBytes = 0;
1765
+ const files: SecureFile[] = [];
1766
+ const shards: KbEvidenceRoutingAuthoringShard[] = [];
1767
+ for (const path of paths) {
1768
+ const file = await secureReadRepositoryFile(
1769
+ root,
1770
+ path,
1771
+ Math.min(MAX_SHARD_BYTES, MAX_TOTAL_SHARD_BYTES - aggregateBytes),
1772
+ `${label} ${path}`,
1773
+ );
1774
+ aggregateBytes += file.bytes.byteLength;
1775
+ if (aggregateBytes > MAX_TOTAL_SHARD_BYTES) {
1776
+ throw new Error(`${label} shards exceed ${MAX_TOTAL_SHARD_BYTES} aggregate bytes.`);
1777
+ }
1778
+ files.push(file);
1779
+ shards.push(parseSchema(
1780
+ kbEvidenceRoutingAuthoringShardSchema,
1781
+ parseJson(file.bytes, `${label} ${path}`),
1782
+ `${label} ${path}`,
1783
+ ));
1784
+ }
1785
+ return Object.freeze({ files: Object.freeze(files), shards: Object.freeze(shards) });
1786
+ }
1787
+
1788
+ async function readPrivateAuthoringShards(
1789
+ root: string,
1790
+ paths: readonly string[],
1791
+ label: string,
1792
+ ): Promise<{
1793
+ readonly files: readonly SecureFile[];
1794
+ readonly shards: readonly KbEvidenceRoutingPrivateAuthoringShard[];
1795
+ }> {
1796
+ let aggregateBytes = 0;
1797
+ const files: SecureFile[] = [];
1798
+ const shards: KbEvidenceRoutingPrivateAuthoringShard[] = [];
1799
+ for (const path of paths) {
1800
+ const file = await secureReadRepositoryFile(
1801
+ root,
1802
+ path,
1803
+ Math.min(MAX_SHARD_BYTES, MAX_TOTAL_SHARD_BYTES - aggregateBytes),
1804
+ `${label} ${path}`,
1805
+ );
1806
+ aggregateBytes += file.bytes.byteLength;
1807
+ if (aggregateBytes > MAX_TOTAL_SHARD_BYTES) {
1808
+ throw new Error(`${label} shards exceed ${MAX_TOTAL_SHARD_BYTES} aggregate bytes.`);
1809
+ }
1810
+ files.push(file);
1811
+ shards.push(parseSchema(
1812
+ kbEvidenceRoutingPrivateAuthoringShardSchema,
1813
+ parseJson(file.bytes, `${label} ${path}`),
1814
+ `${label} ${path}`,
1815
+ ));
1816
+ }
1817
+ return Object.freeze({ files: Object.freeze(files), shards: Object.freeze(shards) });
1818
+ }
1819
+
1820
+ export async function loadKbEvidenceRoutingEvaluationAuthoring(
1821
+ configPathInput: string,
1822
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
1823
+ ): Promise<LoadedKbEvidenceRoutingAuthoring> {
1824
+ const loadedConfig = await loadConfig(configPathInput);
1825
+ const { config } = loadedConfig;
1826
+ const repositoryRoot = await canonicalRepositoryRoot(
1827
+ config.repositoryRoot,
1828
+ loadedConfig.configPath,
1829
+ );
1830
+ const artifactRoot = await canonicalArtifactRoot(dependencies.artifactRoot, repositoryRoot);
1831
+ const runGit = dependencies.runGit ?? runGitCommand;
1832
+ const vaultRoot = confinedRelativePath(config.frozen.vaultRoot, "frozen.vaultRoot");
1833
+ await assertRepositoryComponents(repositoryRoot, vaultRoot, "directory");
1834
+
1835
+ const developmentPaths = config.shards.development.map((path, index) =>
1836
+ requireAuthoringPath(path, `shards.development[${index}]`));
1837
+ const qaPaths = config.shards.qa.map((path, index) =>
1838
+ requireAuthoringPath(path, `shards.qa[${index}]`));
1839
+ const heldOutPaths = config.shards.heldOut.map((path, index) =>
1840
+ requireHeldOutPath(path, `shards.heldOut[${index}]`));
1841
+ assertNoDuplicates(developmentPaths, "shards.development");
1842
+ assertNoDuplicates(qaPaths, "shards.qa");
1843
+ assertNoDuplicates(heldOutPaths, "shards.heldOut");
1844
+ assertNoDuplicates([...developmentPaths, ...qaPaths], "Visible development and QA shard paths");
1845
+
1846
+ const [development, qa, heldOut] = await Promise.all([
1847
+ readAuthoringShards(repositoryRoot, developmentPaths, "development authoring shard"),
1848
+ readAuthoringShards(repositoryRoot, qaPaths, "QA authoring shard"),
1849
+ readPrivateAuthoringShards(artifactRoot, heldOutPaths, "held-out artifact-B shard"),
1850
+ ]);
1851
+ const totalShardBytes = [...development.files, ...qa.files, ...heldOut.files]
1852
+ .reduce((sum, file) => sum + file.bytes.byteLength, 0);
1853
+ if (totalShardBytes > MAX_TOTAL_SHARD_BYTES) {
1854
+ throw new Error(`All development, QA, and held-out shards exceed ${MAX_TOTAL_SHARD_BYTES} aggregate bytes.`);
1855
+ }
1856
+ const joinedHeldOutShards = joinPrivateAuthoringShards(
1857
+ heldOut.shards,
1858
+ kbEvidenceRoutingBuildContractSha256(loadedConfig.file.bytes),
1859
+ config.assessment.assessors.map(({ id }) => id),
1860
+ config.reviewPolicy.sourceFamilyAssignment.reviewerIds,
1861
+ );
1862
+ const shardFiles = Object.freeze([...development.files, ...heldOut.files]);
1863
+ const shards = Object.freeze([...development.shards, ...joinedHeldOutShards]);
1864
+ const shardPayloadFingerprints = [
1865
+ ...development.shards,
1866
+ ...qa.shards,
1867
+ ...heldOut.shards,
1868
+ ].map((shard) => sha256(canonicalJsonBytes(shard)));
1869
+ assertNoDuplicates(shardPayloadFingerprints, "Authoring shard payloads");
1870
+
1871
+ const documentMetadata = shards.flatMap(({ documents }) => documents);
1872
+ const questions = shards.flatMap(({ questions }) => questions);
1873
+ if (questions.length === 0 || questions.length > MAX_EVALUATION_V2_QUERIES) {
1874
+ throw new Error(`Authoring shards must declare from 1 through ${MAX_EVALUATION_V2_QUERIES} questions.`);
1875
+ }
1876
+ assertNoDuplicates(documentMetadata.map(({ sourcePath }) => sourcePath), "Document source paths");
1877
+ for (const [index, metadata] of documentMetadata.entries()) {
1878
+ const sourcePath = confinedRelativePath(metadata.sourcePath, `documents[${index}].sourcePath`);
1879
+ if (!sourcePath.endsWith(".md")) {
1880
+ throw new Error(`documents[${index}].sourcePath must name a Markdown file.`);
1881
+ }
1882
+ }
1883
+ assertNoDuplicates(
1884
+ documentMetadata.flatMap(({ documentId }) => documentId === undefined ? [] : [documentId]),
1885
+ "Explicit document IDs",
1886
+ );
1887
+ assertNoDuplicates(questions.map(({ key }) => key), "Question keys");
1888
+
1889
+ const metadataByPath = new Map(documentMetadata.map((metadata) => [metadata.sourcePath, metadata]));
1890
+ assertExplicitPromotionQuestionDocuments(questions, metadataByPath);
1891
+ const catalogPaths = await frozenMarkdownPaths(config, repositoryRoot, runGit);
1892
+ const missingDeclared = [...metadataByPath.keys()].filter((path) => !catalogPaths.includes(path)).toSorted();
1893
+ if (missingDeclared.length > 0) {
1894
+ throw new Error(`Authoring metadata names Markdown absent from the frozen vault: ${missingDeclared.join(", ")}.`);
1895
+ }
1896
+ let aggregateSourceBytes = 0;
1897
+ const sourceFiles: SourceFile[] = [];
1898
+ const documents: PromotionCorpusMarkdownDocumentV2[] = [];
1899
+ for (const sourcePath of catalogPaths) {
1900
+ const metadata = metadataByPath.get(sourcePath) ?? derivedDocumentMetadata(sourcePath);
1901
+ const repositoryPath = vaultRoot === "." ? sourcePath : `${vaultRoot}/${sourcePath}`;
1902
+ const bytes = await requiredGitOutput(
1903
+ runGit,
1904
+ repositoryRoot,
1905
+ ["show", `${config.frozen.repositoryCommit}:${repositoryPath}`],
1906
+ Math.min(MAX_EVALUATION_EVIDENCE_DOCUMENT_BYTES, MAX_EVALUATION_EVIDENCE_TOTAL_BYTES - aggregateSourceBytes),
1907
+ `frozen Markdown ${sourcePath}`,
1908
+ );
1909
+ const sourceFile: SecureFile = {
1910
+ absolutePath: `${config.frozen.repositoryCommit}:${repositoryPath}`,
1911
+ relativePath: repositoryPath,
1912
+ bytes,
1913
+ sha256: sha256(bytes),
1914
+ };
1915
+ aggregateSourceBytes += sourceFile.bytes.byteLength;
1916
+ if (aggregateSourceBytes > MAX_EVALUATION_EVIDENCE_TOTAL_BYTES) {
1917
+ throw new Error(`Frozen Markdown exceeds ${MAX_EVALUATION_EVIDENCE_TOTAL_BYTES} aggregate bytes.`);
1918
+ }
1919
+ let markdown: string;
1920
+ try {
1921
+ markdown = new TextDecoder("utf-8", { fatal: true }).decode(sourceFile.bytes);
1922
+ } catch {
1923
+ throw new Error(`Frozen Markdown ${sourcePath} is not valid UTF-8.`);
1924
+ }
1925
+ sourceFiles.push({ ...sourceFile, sourcePath, markdown });
1926
+ documents.push({
1927
+ ...(metadata.documentId === undefined ? {} : { documentId: metadata.documentId }),
1928
+ sourcePath,
1929
+ markdown,
1930
+ sourceFamilyKey: metadata.sourceFamilyKey,
1931
+ sourceClass: metadata.sourceClass,
1932
+ trustClass: metadata.trustClass,
1933
+ ...(metadata.sourceFamilyRationale === undefined
1934
+ ? {}
1935
+ : { sourceFamilyRationale: metadata.sourceFamilyRationale }),
1936
+ ...(metadata.sourceFamilyReviewerIds === undefined
1937
+ ? {}
1938
+ : { sourceFamilyReviewerIds: metadata.sourceFamilyReviewerIds }),
1939
+ });
1940
+ }
1941
+
1942
+ assertVisibleHeldOutIsolation({
1943
+ development: development.shards,
1944
+ qa: qa.shards,
1945
+ heldOut: joinedHeldOutShards,
1946
+ sourceSha256ByPath: new Map(sourceFiles.map(({ sourcePath, sha256: digest }) => [
1947
+ sourcePath,
1948
+ digest,
1949
+ ])),
1950
+ ngramSize: config.reviewPolicy.ngramSize ?? 3,
1951
+ crossSplitNgramThreshold: config.reviewPolicy.crossSplitNgramThreshold ?? 0.8,
1952
+ });
1953
+
1954
+ const outputRelativePaths = [
1955
+ config.outputs.corpus,
1956
+ config.outputs.externalSeal,
1957
+ config.outputs.summary,
1958
+ ].map((path, index) => confinedRelativePath(path, `outputs[${index}]`));
1959
+ assertNoDuplicates(outputRelativePaths, "Output paths");
1960
+ const occupiedInputs = new Set(heldOutPaths);
1961
+ for (const outputPath of outputRelativePaths) {
1962
+ if (occupiedInputs.has(outputPath)) {
1963
+ throw new Error(`Output path ${outputPath} collides with an authoring input.`);
1964
+ }
1965
+ }
1966
+ const [corpusOutput, sealOutput, summaryOutput] = await Promise.all(outputRelativePaths.map(
1967
+ (path, index) => validateOutputPath(artifactRoot, path, `outputs[${index}]`),
1968
+ ));
1969
+ if (corpusOutput === undefined || sealOutput === undefined || summaryOutput === undefined) {
1970
+ throw new Error("All three output paths are required.");
1971
+ }
1972
+ const configCanonicalPath = await realpath(loadedConfig.configPath);
1973
+ if ([corpusOutput, sealOutput, summaryOutput].includes(configCanonicalPath)) {
1974
+ throw new Error("An output path must not replace the checked build config.");
1975
+ }
1976
+
1977
+ const reviewPolicy = {
1978
+ ...(config.reviewPolicy.ngramSize === undefined
1979
+ ? {}
1980
+ : { ngramSize: config.reviewPolicy.ngramSize }),
1981
+ ...(config.reviewPolicy.crossSplitNgramThreshold === undefined
1982
+ ? {}
1983
+ : { crossSplitNgramThreshold: config.reviewPolicy.crossSplitNgramThreshold }),
1984
+ ...(config.reviewPolicy.labelPredictabilityCeiling === undefined
1985
+ ? {}
1986
+ : { labelPredictabilityCeiling: config.reviewPolicy.labelPredictabilityCeiling }),
1987
+ sourceFamilyAssignment: config.reviewPolicy.sourceFamilyAssignment,
1988
+ };
1989
+ const input: PromotionCorpusAuthoringInputV2 = {
1990
+ id: config.id,
1991
+ description: config.description,
1992
+ sealedAt: config.sealedAt,
1993
+ buildContractSha256: kbEvidenceRoutingBuildContractSha256(loadedConfig.file.bytes),
1994
+ frozen: config.frozen,
1995
+ assessment: {
1996
+ rubricVersion: config.assessment.rubricVersion,
1997
+ assessors: config.assessment.assessors.map((assessor) => ({
1998
+ id: assessor.id,
1999
+ ...(assessor.displayName === undefined ? {} : { displayName: assessor.displayName }),
2000
+ ...(assessor.affiliation === undefined ? {} : { affiliation: assessor.affiliation }),
2001
+ })),
2002
+ },
2003
+ experiment: config.experiment,
2004
+ documents,
2005
+ questions: questions.map(promotionQuestion),
2006
+ measurementProfiles: config.measurementProfiles,
2007
+ retrievers: config.retrievers,
2008
+ baselineRetrieverId: config.baselineRetrieverId,
2009
+ evidenceParserVersion: config.evidenceParserVersion,
2010
+ reviewPolicy,
2011
+ };
2012
+ const visibleMetadataByPath = partitionDocumentMetadata(
2013
+ [...development.shards, ...qa.shards],
2014
+ "visible development and QA authoring",
2015
+ );
2016
+ const visibleDocuments = sourceFiles.map((source) => {
2017
+ const metadata = visibleMetadataByPath.get(source.sourcePath)
2018
+ ?? derivedDocumentMetadata(source.sourcePath);
2019
+ return {
2020
+ ...(metadata.documentId === undefined ? {} : { documentId: metadata.documentId }),
2021
+ sourcePath: source.sourcePath,
2022
+ markdown: source.markdown,
2023
+ sourceFamilyKey: metadata.sourceFamilyKey,
2024
+ sourceClass: metadata.sourceClass,
2025
+ trustClass: metadata.trustClass,
2026
+ ...(metadata.sourceFamilyRationale === undefined
2027
+ ? {}
2028
+ : { sourceFamilyRationale: metadata.sourceFamilyRationale }),
2029
+ ...(metadata.sourceFamilyReviewerIds === undefined
2030
+ ? {}
2031
+ : { sourceFamilyReviewerIds: metadata.sourceFamilyReviewerIds }),
2032
+ } satisfies PromotionCorpusMarkdownDocumentV2;
2033
+ });
2034
+ const visibleReview = compileOrThrow({
2035
+ ...input,
2036
+ documents: visibleDocuments,
2037
+ questions: [...development.shards, ...qa.shards]
2038
+ .flatMap(({ questions: visibleQuestions }) => visibleQuestions)
2039
+ .map(promotionQuestion),
2040
+ });
2041
+
2042
+ return {
2043
+ configPath: loadedConfig.configPath,
2044
+ configBytes: loadedConfig.file.bytes,
2045
+ config,
2046
+ repositoryRoot,
2047
+ input,
2048
+ visibleReview,
2049
+ shardFiles,
2050
+ qaShardFiles: qa.files,
2051
+ sourceFiles,
2052
+ artifactRoot,
2053
+ outputPaths: {
2054
+ corpus: corpusOutput,
2055
+ externalSeal: sealOutput,
2056
+ summary: summaryOutput,
2057
+ },
2058
+ };
2059
+ }
2060
+
2061
+ export async function compileKbEvidenceRoutingEvaluationAuthoring(
2062
+ configPath: string,
2063
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
2064
+ ): Promise<{
2065
+ readonly loaded: LoadedKbEvidenceRoutingAuthoring;
2066
+ readonly compilation: Extract<PromotionCorpusCompilationResultV2, { readonly ok: true }>;
2067
+ }> {
2068
+ const loaded = await loadKbEvidenceRoutingEvaluationAuthoring(configPath, dependencies);
2069
+ return { loaded, compilation: compileOrThrow(loaded.input) };
2070
+ }
2071
+
2072
+ async function existingOutputBytes(path: string, maximumBytes: number): Promise<Buffer | undefined> {
2073
+ try {
2074
+ return (await secureReadAbsoluteFile(path, maximumBytes, `existing output ${path}`)).bytes;
2075
+ } catch (error) {
2076
+ if (
2077
+ typeof error === "object"
2078
+ && error !== null
2079
+ && "code" in error
2080
+ && error.code === "ENOENT"
2081
+ ) return undefined;
2082
+ throw error;
2083
+ }
2084
+ }
2085
+
2086
+ async function assertInstallable(path: string, bytes: Buffer): Promise<"unchanged" | "missing"> {
2087
+ const existing = await existingOutputBytes(path, Math.max(MAX_OUTPUT_BYTES, bytes.byteLength));
2088
+ if (existing === undefined) return "missing";
2089
+ if (!existing.equals(bytes)) {
2090
+ throw new Error(`Refusing to overwrite non-identical output ${path}. Version the corpus or choose a new path.`);
2091
+ }
2092
+ return "unchanged";
2093
+ }
2094
+
2095
+ async function writeTemporary(path: string, bytes: Buffer): Promise<string> {
2096
+ if (bytes.byteLength > MAX_OUTPUT_BYTES) throw new Error(`Output ${path} exceeds ${MAX_OUTPUT_BYTES} bytes.`);
2097
+ const temporaryPath = resolve(
2098
+ dirname(path),
2099
+ `.${basename(path)}.${process.pid}.${randomBytes(12).toString("hex")}.tmp`,
2100
+ );
2101
+ const handle = await open(temporaryPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
2102
+ try {
2103
+ await handle.writeFile(bytes);
2104
+ await handle.sync();
2105
+ await handle.chmod(0o644);
2106
+ } catch (error) {
2107
+ await handle.close();
2108
+ await unlink(temporaryPath).catch(() => undefined);
2109
+ throw error;
2110
+ }
2111
+ await handle.close();
2112
+ return temporaryPath;
2113
+ }
2114
+
2115
+ async function installNoReplace(path: string, bytes: Buffer): Promise<"installed" | "unchanged"> {
2116
+ const disposition = await assertInstallable(path, bytes);
2117
+ if (disposition === "unchanged") return disposition;
2118
+ const temporaryPath = await writeTemporary(path, bytes);
2119
+ try {
2120
+ try {
2121
+ await link(temporaryPath, path);
2122
+ } catch (error) {
2123
+ if (
2124
+ typeof error === "object"
2125
+ && error !== null
2126
+ && "code" in error
2127
+ && error.code === "EEXIST"
2128
+ ) {
2129
+ const existing = await secureReadAbsoluteFile(path, MAX_OUTPUT_BYTES, `racing output ${path}`);
2130
+ if (!existing.bytes.equals(bytes)) {
2131
+ throw new Error(`Refusing to replace concurrently written non-identical output ${path}.`);
2132
+ }
2133
+ return "unchanged";
2134
+ }
2135
+ throw error;
2136
+ }
2137
+ return "installed";
2138
+ } finally {
2139
+ await unlink(temporaryPath).catch(() => undefined);
2140
+ }
2141
+ }
2142
+
2143
+ async function readCanonicalCorpus(path: string): Promise<{
2144
+ readonly corpus: RetrievalEvaluationCorpusV2;
2145
+ readonly bytes: Buffer;
2146
+ }> {
2147
+ const file = await secureReadAbsoluteFile(path, MAX_OUTPUT_BYTES, "persisted corpus");
2148
+ const corpus = parseRetrievalEvaluationCorpusV2(parseJson(file.bytes, "persisted corpus"), {
2149
+ claimPromotion: false,
2150
+ });
2151
+ const expected = canonicalJsonBytes(corpus);
2152
+ if (!file.bytes.equals(expected)) {
2153
+ throw new Error("Persisted corpus is not canonical JSON with one final newline.");
2154
+ }
2155
+ return { corpus, bytes: file.bytes };
2156
+ }
2157
+
2158
+ async function readCanonicalSeal(path: string): Promise<{
2159
+ readonly seal: Extract<EvaluationExternalCorpusSealV2, { readonly expectedCorpusSha256: string }>;
2160
+ readonly bytes: Buffer;
2161
+ }> {
2162
+ const file = await secureReadAbsoluteFile(path, MAX_OUTPUT_BYTES, "persisted external seal");
2163
+ const seal = parseExternalSeal(parseJson(file.bytes, "persisted external seal"));
2164
+ const expected = canonicalJsonBytes(seal);
2165
+ if (!file.bytes.equals(expected)) {
2166
+ throw new Error("Persisted external seal is not canonical JSON with one final newline.");
2167
+ }
2168
+ return { seal, bytes: file.bytes };
2169
+ }
2170
+
2171
+ async function readCanonicalSummary(path: string): Promise<{
2172
+ readonly summary: KbEvidenceRoutingEvaluationShaSummary;
2173
+ readonly bytes: Buffer;
2174
+ }> {
2175
+ const file = await secureReadAbsoluteFile(path, MAX_OUTPUT_BYTES, "persisted SHA summary");
2176
+ const summary = parseSchema(
2177
+ kbEvidenceRoutingEvaluationShaSummarySchema,
2178
+ parseJson(file.bytes, "persisted SHA summary"),
2179
+ "persisted SHA summary",
2180
+ );
2181
+ const expected = canonicalJsonBytes(summary);
2182
+ if (!file.bytes.equals(expected)) {
2183
+ throw new Error("Persisted SHA summary is not canonical JSON with one final newline.");
2184
+ }
2185
+ return { summary, bytes: file.bytes };
2186
+ }
2187
+
2188
+ function shaSummary(
2189
+ loaded: LoadedKbEvidenceRoutingAuthoring,
2190
+ corpus: RetrievalEvaluationCorpusV2,
2191
+ corpusBytes: Buffer,
2192
+ sealBytes: Buffer,
2193
+ ): KbEvidenceRoutingEvaluationShaSummary {
2194
+ return {
2195
+ schemaVersion: 1,
2196
+ corpus: {
2197
+ path: loaded.config.outputs.corpus,
2198
+ byteLength: corpusBytes.byteLength,
2199
+ outputSha256: sha256(corpusBytes),
2200
+ committedCorpusSha256: corpus.manifest.corpusSha256,
2201
+ },
2202
+ externalSeal: {
2203
+ path: loaded.config.outputs.externalSeal,
2204
+ byteLength: sealBytes.byteLength,
2205
+ outputSha256: sha256(sealBytes),
2206
+ },
2207
+ authoring: {
2208
+ configSha256: kbEvidenceRoutingBuildContractSha256(loaded.configBytes),
2209
+ shards: loaded.shardFiles.map((file) => ({
2210
+ path: file.relativePath ?? file.absolutePath,
2211
+ byteLength: file.bytes.byteLength,
2212
+ sha256: file.sha256,
2213
+ })),
2214
+ qaShards: loaded.qaShardFiles.map((file) => ({
2215
+ path: file.relativePath ?? file.absolutePath,
2216
+ byteLength: file.bytes.byteLength,
2217
+ sha256: file.sha256,
2218
+ })),
2219
+ sources: loaded.sourceFiles.map((file) => ({
2220
+ sourcePath: file.sourcePath,
2221
+ byteLength: file.bytes.byteLength,
2222
+ sha256: file.sha256,
2223
+ })).toSorted((left, right) => left.sourcePath.localeCompare(right.sourcePath)),
2224
+ },
2225
+ counts: {
2226
+ documents: corpus.documents.length,
2227
+ evidenceUnits: corpus.evidenceUnits.length,
2228
+ questions: corpus.queries.length,
2229
+ },
2230
+ visibleReview: {
2231
+ questions: loaded.visibleReview.corpus.queries.length,
2232
+ exactQuotaAndBalanceMet: loaded.visibleReview.diagnostics.quotaLedger.every(({ met }) => met),
2233
+ labelPredictabilityMet: loaded.visibleReview.diagnostics.labelPredictability.met,
2234
+ pairedPowerMet: loaded.visibleReview.diagnostics.pairedPower.met,
2235
+ diagnosticsSha256: sha256(canonicalJsonBytes(loaded.visibleReview.diagnostics)),
2236
+ },
2237
+ };
2238
+ }
2239
+
2240
+ type PersistedPromotionValidation = {
2241
+ readonly loaded: LoadedKbEvidenceRoutingAuthoring;
2242
+ readonly corpus: RetrievalEvaluationCorpusV2;
2243
+ readonly externalSeal: Extract<EvaluationExternalCorpusSealV2, { readonly expectedCorpusSha256: string }>;
2244
+ readonly corpusBytes: Buffer;
2245
+ readonly sealBytes: Buffer;
2246
+ };
2247
+
2248
+ async function validatePersistedPromotion(
2249
+ configPath: string,
2250
+ expected?: Pick<LoadedKbEvidenceRoutingAuthoring, "configBytes" | "outputPaths">,
2251
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
2252
+ ): Promise<PersistedPromotionValidation> {
2253
+ // Re-read the checked config, every shard, and every Markdown source after the seal is
2254
+ // independently persisted. This makes an intervening gold or source edit fail promotion.
2255
+ const loaded = await loadKbEvidenceRoutingEvaluationAuthoring(configPath, dependencies);
2256
+ if (loaded.config.shards.qa.length > 0) {
2257
+ assertKbEvidenceRoutingVisibleReviewReady(loaded.visibleReview.diagnostics);
2258
+ }
2259
+ if (loaded.config.shards.heldOut.length === 0) {
2260
+ throw new Error("Promotion build requires held-out shards supplied only by artifact B.");
2261
+ }
2262
+ await verifyKbEvidenceRoutingFrozenSources(loaded, dependencies);
2263
+ if (expected !== undefined && !loaded.configBytes.equals(expected.configBytes)) {
2264
+ throw new Error("Build config changed after the corpus seal was persisted.");
2265
+ }
2266
+ if (
2267
+ expected !== undefined
2268
+ && (
2269
+ loaded.outputPaths.corpus !== expected.outputPaths.corpus
2270
+ || loaded.outputPaths.externalSeal !== expected.outputPaths.externalSeal
2271
+ || loaded.outputPaths.summary !== expected.outputPaths.summary
2272
+ )
2273
+ ) throw new Error("Output paths changed after the corpus seal was persisted.");
2274
+
2275
+ const [persistedCorpus, persistedSeal] = await Promise.all([
2276
+ readCanonicalCorpus(loaded.outputPaths.corpus),
2277
+ readCanonicalSeal(loaded.outputPaths.externalSeal),
2278
+ ]);
2279
+ if (persistedSeal.seal.expectedCorpusSha256 !== persistedCorpus.corpus.manifest.corpusSha256) {
2280
+ throw new Error("Persisted external seal does not commit the persisted corpus.");
2281
+ }
2282
+ const promotion = compilePromotionCorpusAuthoringV2(loaded.input, persistedSeal.seal);
2283
+ if (!promotion.ok) {
2284
+ throw new Error(promotion.errors.map(({ code, message }) => `${code}: ${message}`).join("\n"));
2285
+ }
2286
+ const promotedBytes = canonicalJsonBytes(promotion.corpus);
2287
+ if (!promotedBytes.equals(persistedCorpus.bytes)) {
2288
+ throw new Error("Persisted corpus bytes differ from the independently validated promotion corpus.");
2289
+ }
2290
+ return {
2291
+ loaded,
2292
+ corpus: promotion.corpus,
2293
+ externalSeal: persistedSeal.seal,
2294
+ corpusBytes: persistedCorpus.bytes,
2295
+ sealBytes: persistedSeal.bytes,
2296
+ };
2297
+ }
2298
+
2299
+ /** Revalidates persisted canonical bytes against freshly read authoring inputs and the external seal. */
2300
+ export async function validatePersistedKbEvidenceRoutingPromotion(
2301
+ configPath: string,
2302
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
2303
+ ): Promise<Pick<PersistedPromotionValidation, "corpus" | "externalSeal">> {
2304
+ const validation = await validatePersistedPromotion(configPath, undefined, dependencies);
2305
+ return { corpus: validation.corpus, externalSeal: validation.externalSeal };
2306
+ }
2307
+
2308
+ /**
2309
+ * Revalidate the complete immutable build, including the summary's commitments
2310
+ * to the freshly read config, visible QA, private labels, frozen sources, corpus,
2311
+ * and independently anchored seal.
2312
+ */
2313
+ export async function validatePersistedKbEvidenceRoutingBuild(
2314
+ configPath: string,
2315
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
2316
+ ): Promise<ValidatedKbEvidenceRoutingEvaluationBuild> {
2317
+ const validation = await validatePersistedPromotion(configPath, undefined, dependencies);
2318
+ const persistedSummary = await readCanonicalSummary(validation.loaded.outputPaths.summary);
2319
+ const expectedSummary = shaSummary(
2320
+ validation.loaded,
2321
+ validation.corpus,
2322
+ validation.corpusBytes,
2323
+ validation.sealBytes,
2324
+ );
2325
+ const expectedBytes = canonicalJsonBytes(expectedSummary);
2326
+ if (!persistedSummary.bytes.equals(expectedBytes)) {
2327
+ throw new Error(
2328
+ "Persisted SHA summary differs from the freshly validated immutable build inputs.",
2329
+ );
2330
+ }
2331
+ return Object.freeze({
2332
+ corpus: validation.corpus,
2333
+ externalSeal: validation.externalSeal,
2334
+ summary: persistedSummary.summary,
2335
+ summaryOutputSha256: sha256(persistedSummary.bytes),
2336
+ });
2337
+ }
2338
+
2339
+ /**
2340
+ * Atomically anchors the independently reviewed corpus digest in artifact B.
2341
+ * This step writes only the seal. It never installs corpus or summary outputs.
2342
+ */
2343
+ export async function anchorKbEvidenceRoutingEvaluationSeal(
2344
+ configPath: string,
2345
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
2346
+ ): Promise<AnchorKbEvidenceRoutingEvaluationSealResult> {
2347
+ const loaded = await loadKbEvidenceRoutingEvaluationAuthoring(configPath, dependencies);
2348
+ if (loaded.config.shards.qa.length > 0) {
2349
+ assertKbEvidenceRoutingVisibleReviewReady(loaded.visibleReview.diagnostics);
2350
+ }
2351
+ if (loaded.config.shards.heldOut.length === 0) {
2352
+ throw new Error("Seal anchoring requires held-out shards supplied only by artifact B.");
2353
+ }
2354
+ await verifyKbEvidenceRoutingFrozenSources(loaded, dependencies);
2355
+ const compilation = compileOrThrow(loaded.input);
2356
+ if (!compilation.diagnostics.promotionLayoutReady) {
2357
+ throw new Error(
2358
+ "Seal anchoring requires a promotion-ready quota, label-predictability, and paired-power design.",
2359
+ );
2360
+ }
2361
+ validatePromotionCorpusDesignV2(compilation.corpus);
2362
+ const promotion = compilePromotionCorpusAuthoringV2(loaded.input, compilation.externalSeal);
2363
+ if (!promotion.ok) {
2364
+ throw new Error(promotion.errors.map(({ code, message }) => `${code}: ${message}`).join("\n"));
2365
+ }
2366
+ if (!canonicalJsonBytes(promotion.corpus).equals(canonicalJsonBytes(compilation.corpus))) {
2367
+ throw new Error("Promotion validation changed the corpus before seal anchoring.");
2368
+ }
2369
+ const sealBytes = canonicalJsonBytes(compilation.externalSeal);
2370
+ if (await existingOutputBytes(loaded.outputPaths.corpus, MAX_OUTPUT_BYTES) !== undefined) {
2371
+ throw new Error("Refusing to anchor a corpus seal after corpus output already exists.");
2372
+ }
2373
+ if (await existingOutputBytes(loaded.outputPaths.summary, MAX_OUTPUT_BYTES) !== undefined) {
2374
+ throw new Error("Refusing to anchor a corpus seal after summary output already exists.");
2375
+ }
2376
+ const install = await installNoReplace(loaded.outputPaths.externalSeal, sealBytes);
2377
+ const persisted = await readCanonicalSeal(loaded.outputPaths.externalSeal);
2378
+ if (!persisted.bytes.equals(sealBytes)) {
2379
+ throw new Error("Persisted external seal differs from the independently anchored canonical bytes.");
2380
+ }
2381
+ return Object.freeze({
2382
+ externalSeal: persisted.seal,
2383
+ outputPath: loaded.config.outputs.externalSeal,
2384
+ outputSha256: sha256(persisted.bytes),
2385
+ install,
2386
+ });
2387
+ }
2388
+
2389
+ export async function buildKbEvidenceRoutingEvaluation(
2390
+ configPath: string,
2391
+ dependencies: KbEvidenceRoutingEvaluationBuildDependencies = {},
2392
+ ): Promise<BuildKbEvidenceRoutingEvaluationResult> {
2393
+ const loaded = await loadKbEvidenceRoutingEvaluationAuthoring(configPath, dependencies);
2394
+ if (loaded.config.shards.qa.length > 0) {
2395
+ assertKbEvidenceRoutingVisibleReviewReady(loaded.visibleReview.diagnostics);
2396
+ }
2397
+ if (loaded.config.shards.heldOut.length === 0) {
2398
+ throw new Error("Promotion build requires held-out shards supplied only by artifact B.");
2399
+ }
2400
+ await verifyKbEvidenceRoutingFrozenSources(loaded, dependencies);
2401
+ const first = { loaded, compilation: compileOrThrow(loaded.input) } as const;
2402
+ const corpusBytes = canonicalJsonBytes(first.compilation.corpus);
2403
+ let persistedSeal: Awaited<ReturnType<typeof readCanonicalSeal>>;
2404
+ try {
2405
+ persistedSeal = await readCanonicalSeal(first.loaded.outputPaths.externalSeal);
2406
+ } catch (error) {
2407
+ if (
2408
+ typeof error === "object"
2409
+ && error !== null
2410
+ && "code" in error
2411
+ && error.code === "ENOENT"
2412
+ ) {
2413
+ throw new Error("External corpus seal is absent. Run the independent --anchor-seal step first.");
2414
+ }
2415
+ throw error;
2416
+ }
2417
+ const promotion = compilePromotionCorpusAuthoringV2(first.loaded.input, persistedSeal.seal);
2418
+ if (!promotion.ok) {
2419
+ throw new Error(promotion.errors.map(({ code, message }) => `${code}: ${message}`).join("\n"));
2420
+ }
2421
+ const promotedBytes = canonicalJsonBytes(promotion.corpus);
2422
+ if (!promotedBytes.equals(corpusBytes)) {
2423
+ throw new Error("Promotion validation changed the compiled corpus bytes.");
2424
+ }
2425
+
2426
+ // The normal build consumes an already anchored seal and never creates or replaces it.
2427
+ await assertInstallable(first.loaded.outputPaths.corpus, corpusBytes);
2428
+ const corpusInstall = await installNoReplace(first.loaded.outputPaths.corpus, corpusBytes);
2429
+
2430
+ const validation = await validatePersistedPromotion(configPath, first.loaded, dependencies);
2431
+
2432
+ const summary = shaSummary(
2433
+ validation.loaded,
2434
+ validation.corpus,
2435
+ validation.corpusBytes,
2436
+ validation.sealBytes,
2437
+ );
2438
+ const summaryBytes = canonicalJsonBytes(summary);
2439
+ const summaryInstall = await installNoReplace(validation.loaded.outputPaths.summary, summaryBytes);
2440
+ const rereadSummary = await secureReadAbsoluteFile(
2441
+ validation.loaded.outputPaths.summary,
2442
+ MAX_OUTPUT_BYTES,
2443
+ "persisted SHA summary",
2444
+ );
2445
+ if (!rereadSummary.bytes.equals(summaryBytes)) {
2446
+ throw new Error("Persisted SHA summary changed after atomic installation.");
2447
+ }
2448
+
2449
+ return {
2450
+ corpus: validation.corpus,
2451
+ externalSeal: validation.externalSeal,
2452
+ summary,
2453
+ summaryOutputSha256: sha256(summaryBytes),
2454
+ installs: {
2455
+ corpus: corpusInstall,
2456
+ externalSeal: "preexisting",
2457
+ summary: summaryInstall,
2458
+ },
2459
+ };
2460
+ }
2461
+
2462
+ export const kbEvidenceRoutingBuildUsage =
2463
+ "Usage: kb-evaluation-builder <--anchor-seal|--build> --config <checked-config.json> --artifact-root <artifact-B>";
2464
+
2465
+ export function parseKbEvidenceRoutingBuildCliArguments(arguments_: readonly string[]): {
2466
+ readonly mode: "anchor-seal" | "build";
2467
+ readonly configPath: string;
2468
+ readonly artifactRoot: string;
2469
+ } {
2470
+ const mode = arguments_[0];
2471
+ const configPath = arguments_[2];
2472
+ const artifactRoot = arguments_[4];
2473
+ if (
2474
+ arguments_.length !== 5
2475
+ || (mode !== "--anchor-seal" && mode !== "--build")
2476
+ || arguments_[1] !== "--config"
2477
+ || arguments_[3] !== "--artifact-root"
2478
+ || configPath === undefined
2479
+ || configPath.trim() === ""
2480
+ || artifactRoot === undefined
2481
+ || artifactRoot.trim() === ""
2482
+ ) {
2483
+ throw new Error(kbEvidenceRoutingBuildUsage);
2484
+ }
2485
+ return Object.freeze({ mode: mode.slice(2) as "anchor-seal" | "build", configPath, artifactRoot });
2486
+ }
2487
+
2488
+ export async function runKbEvidenceRoutingBuildCli(
2489
+ arguments_: readonly string[],
2490
+ dependencies: Pick<KbEvidenceRoutingEvaluationBuildDependencies, "runGit"> = {},
2491
+ ): Promise<AnchorKbEvidenceRoutingEvaluationSealResult | BuildKbEvidenceRoutingEvaluationResult> {
2492
+ const options = parseKbEvidenceRoutingBuildCliArguments(arguments_);
2493
+ if (options.mode === "anchor-seal") {
2494
+ const anchored = await anchorKbEvidenceRoutingEvaluationSeal(options.configPath, {
2495
+ artifactRoot: options.artifactRoot,
2496
+ ...dependencies,
2497
+ });
2498
+ console.log([
2499
+ `External seal ${anchored.install}: ${anchored.outputPath}`,
2500
+ `external-seal-output-sha256=${anchored.outputSha256}`,
2501
+ `committed-corpus-sha256=${anchored.externalSeal.expectedCorpusSha256}`,
2502
+ ].join("\n"));
2503
+ return anchored;
2504
+ }
2505
+ const result = await buildKbEvidenceRoutingEvaluation(options.configPath, {
2506
+ artifactRoot: options.artifactRoot,
2507
+ ...dependencies,
2508
+ });
2509
+ console.log([
2510
+ `Corpus ${result.installs.corpus}: ${result.summary.corpus.path}`,
2511
+ `corpus-output-sha256=${result.summary.corpus.outputSha256}`,
2512
+ `committed-corpus-sha256=${result.summary.corpus.committedCorpusSha256}`,
2513
+ `external-seal-output-sha256=${result.summary.externalSeal.outputSha256}`,
2514
+ `summary-output-sha256=${result.summaryOutputSha256}`,
2515
+ ].join("\n"));
2516
+ return result;
2517
+ }
2518
+
2519
+ if (import.meta.main) {
2520
+ const arguments_ = process.argv.slice(2);
2521
+ if (arguments_.length === 1 && (arguments_[0] === "--help" || arguments_[0] === "-h")) {
2522
+ console.log(kbEvidenceRoutingBuildUsage);
2523
+ } else {
2524
+ await runKbEvidenceRoutingBuildCli(arguments_);
2525
+ }
2526
+ }
2527
+
2528
+ export { EVALUATION_EVIDENCE_PARSER_VERSION };
2529
+ export * from "./evaluation-analysis-v2.js";
2530
+ export * from "./evaluation-corpus-authoring.js";
2531
+ export * from "./evaluation-evidence.js";
2532
+ export * from "./evaluation-execution-v2.js";
2533
+ export * from "./evaluation-implementation.js";
2534
+ export * from "./evaluation-kb-closure.js";
2535
+ export * from "./evaluation-kb-runner-v2.js";
2536
+ export * from "./evaluation-kb-v2.js";
2537
+ export * from "./evaluation-measurement-v2.js";
2538
+ export * from "./evaluation-packing-v2.js";
2539
+ export * from "./evaluation-v2.js";