@hawkeyexl/manni 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +9 -1
  2. package/dist/batch-DFRCLY77.js +36 -0
  3. package/dist/build-Q5C6SX5J.js +31 -0
  4. package/dist/calibrate-XQILKXS3.js +31 -0
  5. package/dist/capture-JQMOC3BW.js +32 -0
  6. package/dist/check-HYNGXYJV.js +18 -0
  7. package/dist/check-HYNGXYJV.js.map +1 -0
  8. package/dist/check-NFZQLJKN.js +27 -0
  9. package/dist/check-NFZQLJKN.js.map +1 -0
  10. package/dist/check-U54XV6BP.js +40 -0
  11. package/dist/check-U54XV6BP.js.map +1 -0
  12. package/dist/chunk-252LNOEG.js +622 -0
  13. package/dist/chunk-252LNOEG.js.map +1 -0
  14. package/dist/chunk-2H7UOFLK.js +11 -0
  15. package/dist/chunk-2H7UOFLK.js.map +1 -0
  16. package/dist/chunk-2U3ORT6D.js +230 -0
  17. package/dist/chunk-2U3ORT6D.js.map +1 -0
  18. package/dist/chunk-3AFUD75K.js +23 -0
  19. package/dist/chunk-3AFUD75K.js.map +1 -0
  20. package/dist/{chunk-3R23C5DT.js → chunk-4TSWU2SU.js} +2 -14
  21. package/dist/chunk-4TSWU2SU.js.map +1 -0
  22. package/dist/chunk-5KHIFVD2.js +1192 -0
  23. package/dist/chunk-5KHIFVD2.js.map +1 -0
  24. package/dist/chunk-5YXBLLAC.js +2165 -0
  25. package/dist/chunk-5YXBLLAC.js.map +1 -0
  26. package/dist/chunk-644JVKJ4.js +3872 -0
  27. package/dist/chunk-644JVKJ4.js.map +1 -0
  28. package/dist/chunk-6OYEI2VV.js +88 -0
  29. package/dist/chunk-6OYEI2VV.js.map +1 -0
  30. package/dist/chunk-77AINBYM.js +19 -0
  31. package/dist/chunk-77AINBYM.js.map +1 -0
  32. package/dist/chunk-77HX34KD.js +1287 -0
  33. package/dist/chunk-77HX34KD.js.map +1 -0
  34. package/dist/{chunk-MOQXWXSG.js → chunk-7QMCHA4N.js} +32 -240
  35. package/dist/chunk-7QMCHA4N.js.map +1 -0
  36. package/dist/chunk-BUXDLRDT.js +455 -0
  37. package/dist/chunk-BUXDLRDT.js.map +1 -0
  38. package/dist/chunk-C6OH6CNF.js +388 -0
  39. package/dist/chunk-C6OH6CNF.js.map +1 -0
  40. package/dist/chunk-CK7SKVHR.js +1042 -0
  41. package/dist/chunk-CK7SKVHR.js.map +1 -0
  42. package/dist/chunk-CYWHFSRT.js +6737 -0
  43. package/dist/chunk-CYWHFSRT.js.map +1 -0
  44. package/dist/chunk-ERMSO34F.js +17 -0
  45. package/dist/chunk-ERMSO34F.js.map +1 -0
  46. package/dist/{chunk-IOYXPBCL.js → chunk-G3T47CFV.js} +1 -7
  47. package/dist/{chunk-IOYXPBCL.js.map → chunk-G3T47CFV.js.map} +1 -1
  48. package/dist/chunk-G7NJSREW.js +979 -0
  49. package/dist/chunk-G7NJSREW.js.map +1 -0
  50. package/dist/chunk-GLP5PSXB.js +72 -0
  51. package/dist/chunk-GLP5PSXB.js.map +1 -0
  52. package/dist/chunk-H2DLH2JI.js +198 -0
  53. package/dist/chunk-H2DLH2JI.js.map +1 -0
  54. package/dist/chunk-H6FZ66XR.js +634 -0
  55. package/dist/chunk-H6FZ66XR.js.map +1 -0
  56. package/dist/{chunk-CI2GOU3A.js → chunk-IEQHSCZG.js} +56 -137
  57. package/dist/chunk-IEQHSCZG.js.map +1 -0
  58. package/dist/chunk-IKEWBMCN.js +98 -0
  59. package/dist/chunk-IKEWBMCN.js.map +1 -0
  60. package/dist/chunk-JOHKIAVH.js +5440 -0
  61. package/dist/chunk-JOHKIAVH.js.map +1 -0
  62. package/dist/chunk-JVTNMVYU.js +10921 -0
  63. package/dist/chunk-JVTNMVYU.js.map +1 -0
  64. package/dist/chunk-K3D6FPV6.js +164 -0
  65. package/dist/chunk-K3D6FPV6.js.map +1 -0
  66. package/dist/{chunk-MFDD3YGU.js → chunk-KCGV52AM.js} +69 -48
  67. package/dist/{chunk-MFDD3YGU.js.map → chunk-KCGV52AM.js.map} +1 -1
  68. package/dist/chunk-LAKAWRV5.js +1651 -0
  69. package/dist/chunk-LAKAWRV5.js.map +1 -0
  70. package/dist/chunk-LJ2RDH3F.js +49 -0
  71. package/dist/chunk-LJ2RDH3F.js.map +1 -0
  72. package/dist/chunk-M2JCK5F6.js +805 -0
  73. package/dist/chunk-M2JCK5F6.js.map +1 -0
  74. package/dist/chunk-MNH7QO4Z.js +15 -0
  75. package/dist/chunk-MNH7QO4Z.js.map +1 -0
  76. package/dist/{chunk-RVVCA4WL.js → chunk-MYC3O5EJ.js} +4 -4
  77. package/dist/chunk-MYMBCTDE.js +387 -0
  78. package/dist/chunk-MYMBCTDE.js.map +1 -0
  79. package/dist/chunk-NCPWXHDO.js +683 -0
  80. package/dist/chunk-NCPWXHDO.js.map +1 -0
  81. package/dist/chunk-NWZ2SLYL.js +2957 -0
  82. package/dist/chunk-NWZ2SLYL.js.map +1 -0
  83. package/dist/chunk-OBFENF3U.js +934 -0
  84. package/dist/chunk-OBFENF3U.js.map +1 -0
  85. package/dist/chunk-OWBRDNRZ.js +16 -0
  86. package/dist/chunk-OWBRDNRZ.js.map +1 -0
  87. package/dist/{chunk-KTSJ2ZHA.js → chunk-PAU352SV.js} +5 -3
  88. package/dist/{chunk-KTSJ2ZHA.js.map → chunk-PAU352SV.js.map} +1 -1
  89. package/dist/chunk-PCE7QHXJ.js +68 -0
  90. package/dist/chunk-PCE7QHXJ.js.map +1 -0
  91. package/dist/chunk-PCHZY2I7.js +299 -0
  92. package/dist/chunk-PCHZY2I7.js.map +1 -0
  93. package/dist/chunk-PHF3D7ER.js +1042 -0
  94. package/dist/chunk-PHF3D7ER.js.map +1 -0
  95. package/dist/chunk-Q35YIDBV.js +1202 -0
  96. package/dist/chunk-Q35YIDBV.js.map +1 -0
  97. package/dist/chunk-RLR4TKUZ.js +4398 -0
  98. package/dist/chunk-RLR4TKUZ.js.map +1 -0
  99. package/dist/chunk-RNVYGWIC.js +30 -0
  100. package/dist/chunk-RNVYGWIC.js.map +1 -0
  101. package/dist/chunk-SE4JN5XN.js +19 -0
  102. package/dist/chunk-SE4JN5XN.js.map +1 -0
  103. package/dist/chunk-TIUTIOFB.js +25 -0
  104. package/dist/chunk-TIUTIOFB.js.map +1 -0
  105. package/dist/chunk-TU2WUBVZ.js +2 -0
  106. package/dist/chunk-TU2WUBVZ.js.map +1 -0
  107. package/dist/chunk-TZOYZK2E.js +98 -0
  108. package/dist/chunk-TZOYZK2E.js.map +1 -0
  109. package/dist/chunk-UKZNLVJW.js +17 -0
  110. package/dist/chunk-UKZNLVJW.js.map +1 -0
  111. package/dist/chunk-UZC7LDAQ.js +178 -0
  112. package/dist/chunk-UZC7LDAQ.js.map +1 -0
  113. package/dist/chunk-V46OILVC.js +3090 -0
  114. package/dist/chunk-V46OILVC.js.map +1 -0
  115. package/dist/chunk-WQE54PNH.js +1975 -0
  116. package/dist/chunk-WQE54PNH.js.map +1 -0
  117. package/dist/chunk-X3BPBDGQ.js +52 -0
  118. package/dist/chunk-X3BPBDGQ.js.map +1 -0
  119. package/dist/chunk-XSLV76YK.js +54 -0
  120. package/dist/chunk-XSLV76YK.js.map +1 -0
  121. package/dist/{chunk-QROCFW5S.js → chunk-XYLUCIKQ.js} +3 -3
  122. package/dist/chunk-YEUYKR62.js +72 -0
  123. package/dist/chunk-YEUYKR62.js.map +1 -0
  124. package/dist/{chunk-ESJ5W7EV.js → chunk-YFQBFNQS.js} +8 -94
  125. package/dist/chunk-YFQBFNQS.js.map +1 -0
  126. package/dist/chunk-YIL4XLLJ.js +1661 -0
  127. package/dist/chunk-YIL4XLLJ.js.map +1 -0
  128. package/dist/chunk-YNHZHSSM.js +41 -0
  129. package/dist/chunk-YNHZHSSM.js.map +1 -0
  130. package/dist/chunk-YTCZRLTU.js +96 -0
  131. package/dist/chunk-YTCZRLTU.js.map +1 -0
  132. package/dist/chunk-YYBFZLAJ.js +374 -0
  133. package/dist/chunk-YYBFZLAJ.js.map +1 -0
  134. package/dist/chunk-ZXRAE55H.js +31 -0
  135. package/dist/chunk-ZXRAE55H.js.map +1 -0
  136. package/dist/cli-2QPR2OZG.js +232 -0
  137. package/dist/cli-2QPR2OZG.js.map +1 -0
  138. package/dist/cli-4IOPGZHR.js +1203 -0
  139. package/dist/cli-4IOPGZHR.js.map +1 -0
  140. package/dist/cli-5FL73WHA.js +346 -0
  141. package/dist/cli-5FL73WHA.js.map +1 -0
  142. package/dist/cli-C262QOWM.js +739 -0
  143. package/dist/cli-C262QOWM.js.map +1 -0
  144. package/dist/cli-LJLLIQB2.js +423 -0
  145. package/dist/cli-LJLLIQB2.js.map +1 -0
  146. package/dist/cli-OFPZ5MAS.js +626 -0
  147. package/dist/cli-OFPZ5MAS.js.map +1 -0
  148. package/dist/cli-SHSMSRQY.js +28 -0
  149. package/dist/cli-SHSMSRQY.js.map +1 -0
  150. package/dist/cli-SRHFUZHA.js +473 -0
  151. package/dist/cli-SRHFUZHA.js.map +1 -0
  152. package/dist/cli-URZQXL4K.js +552 -0
  153. package/dist/cli-URZQXL4K.js.map +1 -0
  154. package/dist/cli.d.ts +12 -2
  155. package/dist/cli.js +109 -7651
  156. package/dist/cli.js.map +1 -1
  157. package/dist/docmeta.js +15 -3
  158. package/dist/docmeta.js.map +1 -1
  159. package/dist/github-FE6KCNYJ.js +27 -0
  160. package/dist/github-FE6KCNYJ.js.map +1 -0
  161. package/dist/github-QXL3VMK2.js +32 -0
  162. package/dist/github-QXL3VMK2.js.map +1 -0
  163. package/dist/index.d.ts +65 -3
  164. package/dist/index.js +407 -337
  165. package/dist/index.js.map +1 -1
  166. package/dist/json-7WV7KL6H.js +17 -0
  167. package/dist/json-7WV7KL6H.js.map +1 -0
  168. package/dist/json-SGZ6ZRFD.js +35 -0
  169. package/dist/json-SGZ6ZRFD.js.map +1 -0
  170. package/dist/lint-DL4LJRAY.js +28 -0
  171. package/dist/lint-DL4LJRAY.js.map +1 -0
  172. package/dist/list-6BAXTZUM.js +29 -0
  173. package/dist/list-6BAXTZUM.js.map +1 -0
  174. package/dist/list-7BKNYPSB.js +31 -0
  175. package/dist/list-7BKNYPSB.js.map +1 -0
  176. package/dist/page-3U2GCFNE.js +41 -0
  177. package/dist/page-3U2GCFNE.js.map +1 -0
  178. package/dist/picker-3XF36MJF.js +14 -0
  179. package/dist/picker-3XF36MJF.js.map +1 -0
  180. package/dist/pretty-2H57QVTY.js +43 -0
  181. package/dist/pretty-2H57QVTY.js.map +1 -0
  182. package/dist/pretty-N6DO4OOL.js +37 -0
  183. package/dist/pretty-N6DO4OOL.js.map +1 -0
  184. package/dist/reporters-M6LJ2IYU.js +21 -0
  185. package/dist/reporters-M6LJ2IYU.js.map +1 -0
  186. package/dist/reporters-N6GHXSS7.js +42 -0
  187. package/dist/reporters-N6GHXSS7.js.map +1 -0
  188. package/dist/reporters-UCNEV7PH.js +71 -0
  189. package/dist/reporters-UCNEV7PH.js.map +1 -0
  190. package/dist/run-XZDAREJE.js +33 -0
  191. package/dist/run-XZDAREJE.js.map +1 -0
  192. package/dist/term-5ICGKPC3.js +1592 -0
  193. package/dist/term-5ICGKPC3.js.map +1 -0
  194. package/dist/types-Y6TXHM7M.js +10 -0
  195. package/dist/types-Y6TXHM7M.js.map +1 -0
  196. package/dist/validate-GQRM77YN.js +16 -0
  197. package/dist/validate-GQRM77YN.js.map +1 -0
  198. package/package.json +1 -1
  199. package/plugin/manni/.claude-plugin/plugin.json +9 -0
  200. package/plugin/manni/hooks/hooks.json +52 -0
  201. package/plugin/manni/skills/check/SKILL.md +20 -0
  202. package/plugin/manni/skills/evals/SKILL.md +27 -0
  203. package/plugin/manni/skills/fix/SKILL.md +56 -0
  204. package/plugin/manni/skills/setup/SKILL.md +41 -0
  205. package/dist/batch-7JJWXWSF.js +0 -18
  206. package/dist/calibrate-IA2ARFMH.js +0 -13
  207. package/dist/capture-C32JXIFN.js +0 -15
  208. package/dist/chunk-3R23C5DT.js.map +0 -1
  209. package/dist/chunk-CI2GOU3A.js.map +0 -1
  210. package/dist/chunk-DP2ZA3WH.js +0 -26518
  211. package/dist/chunk-DP2ZA3WH.js.map +0 -1
  212. package/dist/chunk-ESJ5W7EV.js.map +0 -1
  213. package/dist/chunk-MOQXWXSG.js.map +0 -1
  214. package/dist/chunk-O6RVQKK6.js +0 -27170
  215. package/dist/chunk-O6RVQKK6.js.map +0 -1
  216. package/dist/picker-6HRMETE7.js +0 -12
  217. package/plugin/tracevals/.claude-plugin/plugin.json +0 -9
  218. package/plugin/tracevals/hooks/hooks.json +0 -17
  219. /package/dist/{batch-7JJWXWSF.js.map → batch-DFRCLY77.js.map} +0 -0
  220. /package/dist/{calibrate-IA2ARFMH.js.map → build-Q5C6SX5J.js.map} +0 -0
  221. /package/dist/{capture-C32JXIFN.js.map → calibrate-XQILKXS3.js.map} +0 -0
  222. /package/dist/{picker-6HRMETE7.js.map → capture-JQMOC3BW.js.map} +0 -0
  223. /package/dist/{chunk-RVVCA4WL.js.map → chunk-MYC3O5EJ.js.map} +0 -0
  224. /package/dist/{chunk-QROCFW5S.js.map → chunk-XYLUCIKQ.js.map} +0 -0
@@ -0,0 +1,2957 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isTurnBudgetSkip,
4
+ outputTail,
5
+ parseDuration,
6
+ realExec
7
+ } from "./chunk-LJ2RDH3F.js";
8
+ import {
9
+ EXECUTION_GRANTS,
10
+ NOT_GRANTED_REASON,
11
+ configuredGrants,
12
+ grantsFor,
13
+ isExecutionGrant,
14
+ unknownGrantsMessage
15
+ } from "./chunk-YNHZHSSM.js";
16
+ import {
17
+ DocevalsError
18
+ } from "./chunk-SE4JN5XN.js";
19
+ import {
20
+ declaredAt
21
+ } from "./chunk-H2DLH2JI.js";
22
+ import {
23
+ metaProvenanceEntries
24
+ } from "./chunk-K3D6FPV6.js";
25
+ import {
26
+ familyMarks,
27
+ loadExternalMetadata,
28
+ memberOf,
29
+ mergeWithMarks,
30
+ provenanceEntries
31
+ } from "./chunk-RLR4TKUZ.js";
32
+ import {
33
+ __default,
34
+ classifyRef,
35
+ extractFrontmatter
36
+ } from "./chunk-JVTNMVYU.js";
37
+ import {
38
+ PROVIDERS_KEY,
39
+ assertKnownProvider,
40
+ findConfigFileSync,
41
+ ownsKey,
42
+ parseCollections,
43
+ parseProviders,
44
+ readConfigFileSync,
45
+ selectCollections,
46
+ warn
47
+ } from "./chunk-Q35YIDBV.js";
48
+ import {
49
+ errorMessage
50
+ } from "./chunk-G3T47CFV.js";
51
+
52
+ // src/docevals/core/config.ts
53
+ import { resolve, dirname } from "node:path";
54
+ import { parse as parseYaml } from "yaml";
55
+ import { Ajv2020 } from "ajv/dist/2020.js";
56
+
57
+ // src/docevals/core/config-schema.json
58
+ var config_schema_default = {
59
+ $schema: "https://json-schema.org/draft/2020-12/schema",
60
+ $id: "manni:config:docevals",
61
+ title: "manni docevals configuration",
62
+ description: "The docevals: section of manni.config.yaml. Every manni tool reads its own top-level key; keys that belong to sibling tools are ignored here. Section keys are camelCase, as every manni tool's are. The entries under evals, criteria and suites keep the kebab-case spelling of the frontmatter vocabulary, because they are the same entries a page carries.",
63
+ type: "object",
64
+ properties: {
65
+ docevals: {
66
+ $ref: "#/$defs/docevalsConfig"
67
+ }
68
+ },
69
+ additionalProperties: true,
70
+ $defs: {
71
+ docevalsConfig: {
72
+ type: "object",
73
+ properties: {
74
+ baseline: {
75
+ type: [
76
+ "string",
77
+ "null"
78
+ ],
79
+ description: "Path to the findings baseline, relative to this config file. A baseline records today's findings so a run fails only on new ones (ADR 01017). Null (the default) means no baseline. `--baseline`/`--no-baseline` override this; a bare `--write-baseline` records into *this* path, so a repo that points it somewhere custom cannot accidentally record into a file nothing reads."
80
+ },
81
+ defaults: {
82
+ type: "object",
83
+ properties: {
84
+ suite: {
85
+ type: [
86
+ "string",
87
+ "null"
88
+ ]
89
+ },
90
+ failFast: {
91
+ type: "boolean"
92
+ },
93
+ concurrency: {
94
+ type: "integer",
95
+ minimum: 1
96
+ }
97
+ },
98
+ additionalProperties: false
99
+ },
100
+ provider: {
101
+ type: "string",
102
+ minLength: 1,
103
+ description: "Which provider judges, generates, fills and promotes: auto | anthropic | openai | claude-cli | llama-cpp. Unset, the family's top-level providers.provider decides, then auto, detected exactly as manni meta fill detects: an Anthropic key, then an OpenAI key, then the Claude CLI, then a local model. --provider wins over it, and an ai eval's own provider wins over it for that eval. Connection settings live in the family's top-level providers: map."
104
+ },
105
+ model: {
106
+ type: "string",
107
+ minLength: 1,
108
+ description: "The model, within the provider named beside it. A model is carried only to the provider its own level names: a provider from --provider, an eval or providers.provider that differs from this one takes its own default instead. With no provider here, it goes to whichever provider is in force, which must not be auto, because a model name does not say which provider owns it. Unset (the default) takes the provider's default model from the inference library; manni pins none. --model wins over it."
109
+ },
110
+ judge: {
111
+ type: "object",
112
+ properties: {
113
+ ensembleRuns: {
114
+ type: "integer",
115
+ minimum: 1
116
+ },
117
+ concurrency: {
118
+ type: "integer",
119
+ minimum: 1
120
+ },
121
+ temperature: {
122
+ type: "number",
123
+ minimum: 0
124
+ },
125
+ zones: {
126
+ type: "object",
127
+ properties: {
128
+ autoPass: {
129
+ type: "number",
130
+ minimum: 0,
131
+ maximum: 1
132
+ },
133
+ autoFail: {
134
+ type: "number",
135
+ minimum: 0,
136
+ maximum: 1
137
+ }
138
+ },
139
+ additionalProperties: false
140
+ },
141
+ falsePositiveAlert: {
142
+ type: "number",
143
+ minimum: 0,
144
+ maximum: 1
145
+ },
146
+ cacheDir: {
147
+ type: "string"
148
+ },
149
+ maxTurns: {
150
+ $ref: "#/$defs/maxTurns"
151
+ },
152
+ chunkChars: {
153
+ type: "integer",
154
+ minimum: 1,
155
+ default: 12e3,
156
+ description: "Characters of page content per judge call. A page longer than this is graded by gathering evidence from each part and judging the collected evidence once."
157
+ }
158
+ },
159
+ additionalProperties: false
160
+ },
161
+ scripts: {
162
+ type: "object",
163
+ properties: {
164
+ dir: {
165
+ type: "string"
166
+ },
167
+ configDir: {
168
+ type: "string"
169
+ },
170
+ timeoutMs: {
171
+ type: "integer",
172
+ minimum: 1
173
+ }
174
+ },
175
+ additionalProperties: false
176
+ },
177
+ execution: {
178
+ type: "object",
179
+ additionalProperties: false,
180
+ properties: {
181
+ allow: {
182
+ type: "array",
183
+ items: {
184
+ enum: [
185
+ "frontmatter-commands"
186
+ ]
187
+ },
188
+ default: ["frontmatter-commands"],
189
+ description: "Which content-authored code this corpus may execute. Absent, every grant holds; list fewer to narrow it, or write [] to run none. frontmatter-commands lets `command` evals declared in page frontmatter run. That is code a page author controls, so a run over an untrusted pull request passes --no-execution."
190
+ }
191
+ },
192
+ description: "Operator grant for executing content-authored code."
193
+ },
194
+ fill: {
195
+ type: "object",
196
+ properties: {
197
+ confidenceThreshold: {
198
+ type: "number",
199
+ minimum: 0,
200
+ maximum: 1
201
+ },
202
+ maxEvalsPerPage: {
203
+ type: "integer",
204
+ minimum: 1
205
+ },
206
+ temperature: {
207
+ type: "number",
208
+ minimum: 0
209
+ },
210
+ cacheDir: {
211
+ type: "string"
212
+ },
213
+ maxTurns: {
214
+ $ref: "#/$defs/maxTurns"
215
+ },
216
+ chunkChars: {
217
+ type: "integer",
218
+ minimum: 1,
219
+ default: 12e3,
220
+ description: "Characters of page content per fill call. A longer page is proposed against in parts and the proposals merged by id, keeping the highest confidence."
221
+ }
222
+ },
223
+ additionalProperties: false
224
+ },
225
+ evals: {
226
+ type: "object",
227
+ propertyNames: {
228
+ pattern: "^[a-z0-9][a-z0-9-]*$"
229
+ },
230
+ additionalProperties: {
231
+ $ref: "#/$defs/evalDef"
232
+ },
233
+ description: "The central library of named evals. The map key is the eval's id \u2014 the same id a page's `use:` reference names."
234
+ },
235
+ criteria: {
236
+ type: "object",
237
+ propertyNames: {
238
+ pattern: "^[a-z0-9][a-z0-9-]*$"
239
+ },
240
+ additionalProperties: {
241
+ $ref: "#/$defs/criterionDef"
242
+ },
243
+ description: "Named groups of evals scored as one unit. A criterion contributes a single weighted outcome to its suite and its members contribute none, so writing three checks as a group does not let them outvote three standalone evals."
244
+ },
245
+ suites: {
246
+ type: "object",
247
+ propertyNames: {
248
+ pattern: "^[a-z0-9][a-z0-9-]*$"
249
+ },
250
+ additionalProperties: {
251
+ $ref: "#/$defs/suiteDef"
252
+ }
253
+ }
254
+ },
255
+ additionalProperties: false
256
+ },
257
+ maxTurns: {
258
+ type: [
259
+ "integer",
260
+ "null"
261
+ ],
262
+ minimum: 1,
263
+ description: "Stop after this many turns. Counts work, not pages. Under `judge` a turn is one ensemble run, so a judged eval spends `judge.ensembleRuns` of them; a run may make a second provider call when the first response fails schema validation, so this is a floor on calls rather than an exact count. Under `fill` a turn is exactly one call. A cached result spends none. Null (the default) is unbounded. Replaces the `max-cost-usd` ceiling, which could not be honored exactly under concurrency \u2014 see ADR 01019."
264
+ },
265
+ evalDef: {
266
+ type: "object",
267
+ properties: {
268
+ assertion: {
269
+ type: "string",
270
+ minLength: 1
271
+ },
272
+ type: {
273
+ enum: [
274
+ "capability",
275
+ "regression"
276
+ ]
277
+ },
278
+ grader: {
279
+ type: "string",
280
+ pattern: "^(ai|command|human|tool:[a-z0-9][a-z0-9-]*)$"
281
+ },
282
+ provider: {
283
+ type: "string",
284
+ minLength: 1
285
+ },
286
+ model: {
287
+ type: "string",
288
+ minLength: 1,
289
+ description: "Model judging this ai eval, within provider. A CLI --model still wins over it. Meaningful only on ai evals."
290
+ },
291
+ runs: {
292
+ type: "integer",
293
+ minimum: 1,
294
+ maximum: 50,
295
+ description: "Ensemble runs for this eval, overriding judge.ensembleRuns. A CLI --runs still wins over it. Capped because it multiplies judge cost directly. Meaningful only on ai evals."
296
+ },
297
+ evidence: {
298
+ type: "string",
299
+ minLength: 1
300
+ },
301
+ target: {
302
+ oneOf: [
303
+ {
304
+ enum: [
305
+ "body",
306
+ "raw",
307
+ "frontmatter"
308
+ ]
309
+ },
310
+ {
311
+ type: "object",
312
+ additionalProperties: false,
313
+ required: [
314
+ "source",
315
+ "path"
316
+ ],
317
+ properties: {
318
+ source: {
319
+ const: "file"
320
+ },
321
+ path: {
322
+ type: "string",
323
+ minLength: 1
324
+ }
325
+ }
326
+ }
327
+ ],
328
+ default: "body",
329
+ description: "Which bytes the grader receives: the page body with frontmatter stripped (body), the file verbatim (raw), the parsed frontmatter alone (frontmatter), or a companion file beside the page. Distinct from evidence, which hints where to look within what is graded."
330
+ },
331
+ examples: {
332
+ type: "object",
333
+ properties: {
334
+ pass: {
335
+ $ref: "#/$defs/anchorList"
336
+ },
337
+ fail: {
338
+ $ref: "#/$defs/anchorList"
339
+ }
340
+ },
341
+ additionalProperties: false
342
+ },
343
+ command: {
344
+ type: "array",
345
+ items: {
346
+ type: "string",
347
+ minLength: 1
348
+ },
349
+ minItems: 1
350
+ },
351
+ "success-exit-codes": {
352
+ type: "array",
353
+ minItems: 1,
354
+ items: {
355
+ type: "integer"
356
+ }
357
+ },
358
+ "timeout-ms": {
359
+ type: "integer",
360
+ minimum: 1
361
+ },
362
+ "generated-assertion-hash": {
363
+ type: "string"
364
+ },
365
+ options: {
366
+ type: "object"
367
+ },
368
+ severity: {
369
+ enum: [
370
+ "notice",
371
+ "warning",
372
+ "error"
373
+ ]
374
+ },
375
+ weight: {
376
+ type: "number",
377
+ exclusiveMinimum: 0,
378
+ default: 1,
379
+ description: "Relative contribution to its suite's pass rate. Does not change this eval's own pass/fail: the eval still passes or fails on its own terms, and weight only decides how much that outcome moves the suite total against target-pass-rate. Zero is excluded deliberately - a weightless eval is a silent disable, and skip already means that, loudly."
380
+ }
381
+ },
382
+ additionalProperties: false,
383
+ dependentRequired: {
384
+ "generated-assertion-hash": [
385
+ "command"
386
+ ]
387
+ },
388
+ allOf: [
389
+ {
390
+ if: {
391
+ properties: {
392
+ grader: {
393
+ const: "ai"
394
+ }
395
+ },
396
+ required: [
397
+ "grader"
398
+ ]
399
+ },
400
+ then: {
401
+ required: [
402
+ "assertion"
403
+ ]
404
+ }
405
+ },
406
+ {
407
+ if: {
408
+ not: {
409
+ required: [
410
+ "grader"
411
+ ]
412
+ }
413
+ },
414
+ then: {
415
+ required: [
416
+ "assertion"
417
+ ]
418
+ }
419
+ },
420
+ {
421
+ if: {
422
+ properties: {
423
+ grader: {
424
+ const: "command"
425
+ }
426
+ },
427
+ required: [
428
+ "grader"
429
+ ]
430
+ },
431
+ then: {
432
+ anyOf: [
433
+ {
434
+ required: [
435
+ "assertion"
436
+ ]
437
+ },
438
+ {
439
+ required: [
440
+ "command"
441
+ ]
442
+ }
443
+ ]
444
+ }
445
+ },
446
+ {
447
+ if: {
448
+ properties: {
449
+ grader: {
450
+ const: "human"
451
+ }
452
+ },
453
+ required: [
454
+ "grader"
455
+ ]
456
+ },
457
+ then: {
458
+ required: [
459
+ "assertion"
460
+ ]
461
+ }
462
+ },
463
+ {
464
+ if: {
465
+ required: [
466
+ "command"
467
+ ]
468
+ },
469
+ then: {
470
+ properties: {
471
+ grader: {
472
+ const: "command"
473
+ }
474
+ },
475
+ required: [
476
+ "grader"
477
+ ]
478
+ }
479
+ },
480
+ {
481
+ if: {
482
+ anyOf: [
483
+ {
484
+ required: [
485
+ "success-exit-codes"
486
+ ]
487
+ },
488
+ {
489
+ required: [
490
+ "timeout-ms"
491
+ ]
492
+ }
493
+ ]
494
+ },
495
+ then: {
496
+ properties: {
497
+ grader: {
498
+ const: "command"
499
+ }
500
+ },
501
+ required: [
502
+ "grader"
503
+ ]
504
+ }
505
+ }
506
+ ]
507
+ },
508
+ anchorList: {
509
+ if: {
510
+ type: "array"
511
+ },
512
+ then: {
513
+ type: "array",
514
+ minItems: 1,
515
+ uniqueItems: true,
516
+ items: {
517
+ type: "string",
518
+ minLength: 1
519
+ }
520
+ },
521
+ else: {
522
+ type: "string",
523
+ minLength: 1
524
+ }
525
+ },
526
+ suiteDef: {
527
+ type: "object",
528
+ properties: {
529
+ "target-pass-rate": {
530
+ type: "number",
531
+ minimum: 0,
532
+ maximum: 1
533
+ },
534
+ evals: {
535
+ type: "array",
536
+ items: {
537
+ type: "string"
538
+ }
539
+ },
540
+ criteria: {
541
+ type: "array",
542
+ items: {
543
+ type: "string",
544
+ pattern: "^[a-z0-9][a-z0-9-]*$"
545
+ },
546
+ description: "Criteria scored in this suite, alongside `evals`."
547
+ }
548
+ },
549
+ additionalProperties: false
550
+ },
551
+ criterionDef: {
552
+ type: "object",
553
+ additionalProperties: false,
554
+ required: [
555
+ "evals"
556
+ ],
557
+ properties: {
558
+ evals: {
559
+ type: "array",
560
+ minItems: 1,
561
+ items: {
562
+ type: "string",
563
+ pattern: "^[a-z0-9][a-z0-9-]*$"
564
+ },
565
+ description: "Ids of the evals this criterion is made of."
566
+ },
567
+ combine: {
568
+ enum: [
569
+ "all",
570
+ "any"
571
+ ],
572
+ default: "all",
573
+ description: "all: every member must pass. any: one passing member is enough."
574
+ },
575
+ weight: {
576
+ type: "number",
577
+ exclusiveMinimum: 0,
578
+ default: 1,
579
+ description: "The criterion's contribution to its suite's pass rate."
580
+ }
581
+ }
582
+ }
583
+ }
584
+ };
585
+
586
+ // src/docevals/core/split.ts
587
+ var DEFAULT_CHUNK_CHARS = 12e3;
588
+ function splitBody(body, chunkChars) {
589
+ if (body.length <= chunkChars) return [body];
590
+ const chunks = [];
591
+ let at = 0;
592
+ while (at < body.length) {
593
+ const end = Math.min(at + chunkChars, body.length);
594
+ let cut = end;
595
+ if (end < body.length) {
596
+ const nl = body.lastIndexOf("\n", end - 1);
597
+ if (nl > at) cut = nl + 1;
598
+ }
599
+ chunks.push(body.slice(at, cut));
600
+ at = cut;
601
+ }
602
+ return chunks;
603
+ }
604
+ function looksLikeOverflow(message) {
605
+ return /context|too long|too large|token limit|maximum.*tokens|exceeds/i.test(
606
+ message
607
+ );
608
+ }
609
+ function partLabel(index, total) {
610
+ return `part ${String(index + 1)} of ${String(total)}`;
611
+ }
612
+
613
+ // src/docevals/core/config.ts
614
+ function anchorList(v) {
615
+ if (v === void 0) return void 0;
616
+ return Array.isArray(v) ? v : [v];
617
+ }
618
+ function normalizeEvalDef(raw) {
619
+ const examples = raw.examples === void 0 ? void 0 : {
620
+ ...anchorList(raw.examples.pass) !== void 0 && {
621
+ pass: anchorList(raw.examples.pass)
622
+ },
623
+ ...anchorList(raw.examples.fail) !== void 0 && {
624
+ fail: anchorList(raw.examples.fail)
625
+ }
626
+ };
627
+ return {
628
+ assertion: raw.assertion,
629
+ type: raw.type,
630
+ grader: raw.grader,
631
+ provider: raw.provider,
632
+ evidence: raw.evidence,
633
+ examples,
634
+ command: raw.command,
635
+ successExitCodes: raw["success-exit-codes"],
636
+ timeoutMs: raw["timeout-ms"],
637
+ generatedAssertionHash: raw["generated-assertion-hash"],
638
+ options: raw.options,
639
+ severity: raw.severity,
640
+ weight: raw.weight,
641
+ target: raw.target,
642
+ model: raw.model,
643
+ runs: raw.runs
644
+ };
645
+ }
646
+ var NAMESPACE = "docevals";
647
+ var DEFAULT_CONFIG_FILENAME = "manni.config.yaml";
648
+ var PRE_RENAME_ROOT_KEYS = [
649
+ "version",
650
+ "files",
651
+ "defaults",
652
+ "provider",
653
+ "judge",
654
+ "scripts",
655
+ "fill",
656
+ "evals",
657
+ "suites"
658
+ ];
659
+ var ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, verbose: true });
660
+ var validateConfig = ajv.compile(config_schema_default);
661
+ function isAssertionHashWrapper(value) {
662
+ return value != null && typeof value === "object" && !Array.isArray(value) && "assertionHash" in value;
663
+ }
664
+ function findPreKebabKeys(node, path) {
665
+ if (node == null || typeof node !== "object" || Array.isArray(node)) return [];
666
+ const found = [];
667
+ for (const [key, value] of Object.entries(node)) {
668
+ if (/[a-z0-9][A-Z]/.test(key)) {
669
+ found.push({
670
+ at: `${path}.${key}`,
671
+ becomes: key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
672
+ });
673
+ }
674
+ if (key === "generated" && isAssertionHashWrapper(value)) {
675
+ found.push({ at: `${path}.generated`, becomes: "generated-assertion-hash" });
676
+ continue;
677
+ }
678
+ found.push(...findPreKebabKeys(value, `${path}.${key}`));
679
+ }
680
+ return found;
681
+ }
682
+ var ENTRY_SECTIONS = ["evals", "criteria", "suites"];
683
+ function camelCaseHint(key, parentSchema) {
684
+ const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
685
+ if (camel === key || parentSchema === null || typeof parentSchema !== "object") return "";
686
+ const properties = parentSchema.properties;
687
+ return properties !== null && typeof properties === "object" && Object.hasOwn(properties, camel) ? `; did you mean "${camel}"?` : "";
688
+ }
689
+ function movedProviderHint(instancePath, keyword) {
690
+ return instancePath === `/${NAMESPACE}/provider` && keyword === "type" ? `; "provider" is now a provider name; per-provider settings moved to the top-level providers: map` : "";
691
+ }
692
+ function parseConfig(text, configPath) {
693
+ let raw;
694
+ try {
695
+ raw = parseYaml(text);
696
+ } catch (e) {
697
+ throw new DocevalsError(
698
+ `Invalid YAML in ${configPath}: ${errorMessage(e)}`
699
+ );
700
+ }
701
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
702
+ throw new DocevalsError(`Invalid config in ${configPath}: root must be an object`);
703
+ }
704
+ const doc = raw;
705
+ const wrapped = NAMESPACE in doc;
706
+ const toError = (message) => new DocevalsError(message);
707
+ const collections = Object.hasOwn(doc, COLLECTIONS_KEY) ? parseCollections(doc[COLLECTIONS_KEY], configPath, toError) : [];
708
+ const providers = Object.hasOwn(doc, PROVIDERS_KEY) ? parseProviders(doc[PROVIDERS_KEY], configPath, dirname(resolve(configPath)), toError) : {};
709
+ return parseConfigSection(wrapped ? doc[NAMESPACE] : doc, configPath, wrapped, {
710
+ source: configPath,
711
+ collections,
712
+ providers
713
+ });
714
+ }
715
+ var COLLECTIONS_KEY = "collections";
716
+ var CONFIG_REF = "https://hawkeyexl.github.io/manni/meta/reference/configuration/";
717
+ var MOVED_KEY = "files";
718
+ var MOVED_PROVIDERS_KEY = "providers";
719
+ function parseConfigSection(value, configPath, wrapped, file = { source: configPath, collections: [] }) {
720
+ const raw = wrapped ? { [NAMESPACE]: value ?? {} } : value ?? {};
721
+ if (typeof raw !== "object" || Array.isArray(raw)) {
722
+ throw new DocevalsError(`Invalid config in ${configPath}: root must be an object`);
723
+ }
724
+ if (!(NAMESPACE in raw)) {
725
+ const stray = PRE_RENAME_ROOT_KEYS.filter((k) => k in raw);
726
+ if (stray.length > 0) {
727
+ throw new DocevalsError(
728
+ `Invalid config in ${configPath}: found ${stray.join(", ")} at the root, but no "${NAMESPACE}:" key.
729
+ manni.config.yaml is shared across the manni family, so every docevals setting belongs under a top-level "${NAMESPACE}:" key. Indent the file's contents one level and add that key.`
730
+ );
731
+ }
732
+ }
733
+ const section = raw[NAMESPACE];
734
+ if (section !== null && typeof section === "object" && !Array.isArray(section) && Object.hasOwn(section, MOVED_KEY)) {
735
+ throw new DocevalsError(
736
+ `${file.source}: "${MOVED_KEY}" is no longer a docevals key. Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
737
+ );
738
+ }
739
+ if (section !== null && typeof section === "object" && !Array.isArray(section) && Object.hasOwn(section, MOVED_PROVIDERS_KEY)) {
740
+ throw new DocevalsError(
741
+ `${file.source}: "${MOVED_PROVIDERS_KEY}" is no longer a docevals key. Provider settings are declared once for every tool, under a top-level providers: map. See ${CONFIG_REF}#providers`
742
+ );
743
+ }
744
+ const preKebab = ENTRY_SECTIONS.flatMap(
745
+ (name) => section !== null && typeof section === "object" && !Array.isArray(section) ? findPreKebabKeys(section[name], `${NAMESPACE}.${name}`) : []
746
+ );
747
+ if (preKebab.length > 0) {
748
+ throw new DocevalsError(
749
+ `Invalid config in ${configPath}: camelCase keys are not read in eval, criterion or suite entries.
750
+ ` + preKebab.map((p) => ` ${p.at} -> ${p.becomes}`).join("\n") + `
751
+ Those entries are kebab-case, matching the frontmatter vocabulary.`
752
+ );
753
+ }
754
+ const ns = raw[NAMESPACE];
755
+ const scriptsSection = ns && typeof ns === "object" ? ns.scripts : void 0;
756
+ if (scriptsSection && typeof scriptsSection === "object" && "allow-frontmatter-commands" in scriptsSection) {
757
+ throw new DocevalsError(
758
+ `Invalid config in ${configPath}: scripts.allow-frontmatter-commands has been replaced by execution.allow.
759
+ Command evals run unless it narrows them; write \`execution: { allow: [] }\` to stop them.`
760
+ );
761
+ }
762
+ const unknownGrants = configuredGrants(ns).filter((g) => !isExecutionGrant(g));
763
+ if (unknownGrants.length > 0) {
764
+ throw new DocevalsError(
765
+ `Invalid config in ${configPath}: ${unknownGrantsMessage(unknownGrants)}`
766
+ );
767
+ }
768
+ if (!validateConfig(raw)) {
769
+ const details = (validateConfig.errors ?? []).map((e) => {
770
+ const extra = e.keyword === "additionalProperties" ? e.params.additionalProperty : void 0;
771
+ if (extra !== void 0) {
772
+ return ` ${e.instancePath || "/"}: unknown key "${extra}"${camelCaseHint(extra, e.parentSchema)}`;
773
+ }
774
+ return ` ${e.instancePath || "/"}: ${e.message ?? "is invalid"}${movedProviderHint(e.instancePath, e.keyword)}`;
775
+ }).join("\n");
776
+ throw new DocevalsError(`Invalid config in ${configPath}:
777
+ ${details}`);
778
+ }
779
+ const r = raw[NAMESPACE] ?? {};
780
+ const abs = resolve(configPath);
781
+ const dir = dirname(abs);
782
+ const provider = r.provider ?? null;
783
+ if (provider !== null) assertKnownProvider(provider, (message) => new DocevalsError(message));
784
+ const suites = {};
785
+ for (const [name, def] of Object.entries(r.suites ?? {})) {
786
+ suites[name] = {
787
+ targetPassRate: def["target-pass-rate"] ?? 1,
788
+ evals: def.evals ?? [],
789
+ criteria: def.criteria ?? []
790
+ };
791
+ }
792
+ const criteria = {};
793
+ for (const [name, def] of Object.entries(r.criteria ?? {})) {
794
+ criteria[name] = {
795
+ evals: def.evals ?? [],
796
+ combine: def.combine ?? "all",
797
+ weight: def.weight ?? 1
798
+ };
799
+ }
800
+ const config = {
801
+ collections: file.collections,
802
+ configSource: file.source,
803
+ defaults: {
804
+ suite: r.defaults?.suite ?? null,
805
+ failFast: r.defaults?.failFast ?? false,
806
+ concurrency: r.defaults?.concurrency ?? 4
807
+ },
808
+ provider,
809
+ model: r.model ?? null,
810
+ providers: file.providers ?? {},
811
+ baseline: r.baseline ?? null,
812
+ judge: {
813
+ ensembleRuns: r.judge?.ensembleRuns ?? 3,
814
+ // Falls back to the corpus-wide setting, so an unset value behaves
815
+ // exactly as it did before this knob existed. It is separable because
816
+ // the judge's right parallelism is not the deterministic graders': a
817
+ // local in-process model serves one context at a time (ADR 01039).
818
+ concurrency: r.judge?.concurrency ?? r.defaults?.concurrency ?? 4,
819
+ temperature: r.judge?.temperature ?? 0,
820
+ zones: {
821
+ autoPass: r.judge?.zones?.autoPass ?? 0.8,
822
+ autoFail: r.judge?.zones?.autoFail ?? 0.8
823
+ },
824
+ falsePositiveAlert: r.judge?.falsePositiveAlert ?? 0.15,
825
+ cacheDir: r.judge?.cacheDir ?? ".manni/docevals/cache",
826
+ maxTurns: r.judge?.maxTurns ?? null,
827
+ chunkChars: r.judge?.chunkChars ?? DEFAULT_CHUNK_CHARS
828
+ },
829
+ scripts: {
830
+ dir: r.scripts?.dir ?? "{docDir}/manni-docevals",
831
+ configDir: r.scripts?.configDir ?? "manni-docevals-scripts",
832
+ timeoutMs: r.scripts?.timeoutMs ?? 3e4
833
+ },
834
+ execution: { allow: [...r.execution?.allow ?? EXECUTION_GRANTS] },
835
+ fill: {
836
+ confidenceThreshold: r.fill?.confidenceThreshold ?? 0.7,
837
+ maxEvalsPerPage: r.fill?.maxEvalsPerPage ?? 3,
838
+ temperature: r.fill?.temperature ?? 0,
839
+ cacheDir: r.fill?.cacheDir ?? ".manni/docevals/cache/fill",
840
+ maxTurns: r.fill?.maxTurns ?? null,
841
+ chunkChars: r.fill?.chunkChars ?? DEFAULT_CHUNK_CHARS
842
+ },
843
+ evals: Object.fromEntries(
844
+ Object.entries(r.evals ?? {}).map(([name, def]) => [
845
+ name,
846
+ normalizeEvalDef(def)
847
+ ])
848
+ ),
849
+ criteria,
850
+ suites,
851
+ configPath: abs,
852
+ configDir: dir
853
+ };
854
+ for (const [suiteName, suite] of Object.entries(config.suites)) {
855
+ for (const evalName of suite.evals) {
856
+ if (!(evalName in config.evals)) {
857
+ throw new DocevalsError(
858
+ `Invalid config in ${configPath}: suite "${suiteName}" references undefined eval "${evalName}"`
859
+ );
860
+ }
861
+ }
862
+ }
863
+ for (const [critName, crit] of Object.entries(config.criteria)) {
864
+ for (const evalName of crit.evals) {
865
+ if (!(evalName in config.evals)) {
866
+ throw new DocevalsError(
867
+ `Invalid config in ${configPath}: criterion "${critName}" references undefined eval "${evalName}"`
868
+ );
869
+ }
870
+ }
871
+ }
872
+ for (const [suiteName, suite] of Object.entries(config.suites)) {
873
+ for (const critName of suite.criteria) {
874
+ if (!(critName in config.criteria)) {
875
+ throw new DocevalsError(
876
+ `Invalid config in ${configPath}: suite "${suiteName}" references undefined criterion "${critName}"`
877
+ );
878
+ }
879
+ }
880
+ }
881
+ if (config.defaults.suite && !(config.defaults.suite in config.suites)) {
882
+ throw new DocevalsError(
883
+ `Invalid config in ${configPath}: defaults.suite "${config.defaults.suite}" is not a defined suite`
884
+ );
885
+ }
886
+ return config;
887
+ }
888
+ function loadConfig(path, cwd = process.cwd()) {
889
+ const file = path ? readConfigFileSync(path, cwd, CONFIG_FILE) : findConfigFileSync(cwd, CONFIG_FILE);
890
+ if (file === null) return defaultConfig(cwd);
891
+ return parseConfigSection(file.value, file.path, file.wrapped, {
892
+ source: file.source,
893
+ collections: file.collections,
894
+ providers: file.providers ?? {}
895
+ });
896
+ }
897
+ function defaultConfig(cwd = process.cwd()) {
898
+ return {
899
+ ...parseConfigSection(null, resolve(cwd, DEFAULT_CONFIG_FILENAME), false),
900
+ configSource: null
901
+ };
902
+ }
903
+ function loadRunConfig(opts, cwd = process.cwd()) {
904
+ assertCollectionWithoutPaths(opts.collection, opts.paths);
905
+ return opts.noConfig ? defaultConfig(cwd) : loadConfig(opts.configPath, cwd);
906
+ }
907
+ function assertCollectionWithoutPaths(collection, paths) {
908
+ if ((collection ?? []).length > 0 && (paths ?? []).length > 0) {
909
+ throw new DocevalsError(
910
+ "--collection selects a configured collection; it cannot be combined with paths."
911
+ );
912
+ }
913
+ }
914
+ var CONFIG_FILE = {
915
+ section: NAMESPACE,
916
+ // docevals never shipped a file of its own under manni, so, like cite and
917
+ // a11y, it has no pre-family name to read.
918
+ legacyNames: [],
919
+ toError: (message) => new DocevalsError(message)
920
+ };
921
+
922
+ // src/docevals/core/discover.ts
923
+ import { readFileSync, statSync } from "node:fs";
924
+ import { resolve as resolve2, relative, extname } from "node:path";
925
+ import fg from "fast-glob";
926
+ var FENCES = [
927
+ { format: "yaml", open: /^---\r?\n/, isClose: (l) => l === "---" || l === "..." },
928
+ { format: "toml", open: /^\+\+\+\r?\n/, isClose: (l) => l === "+++" },
929
+ { format: "json", open: /^;;;\r?\n/, isClose: (l) => l === ";;;" }
930
+ ];
931
+ function leadingFrontmatterFormat(content) {
932
+ const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
933
+ return FENCES.find((f) => f.open.test(body))?.format;
934
+ }
935
+ function stripFrontmatterBlock(content) {
936
+ const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
937
+ const fence = FENCES.find((f) => f.open.test(body));
938
+ if (!fence) return body;
939
+ const [openLine = "", ...rest] = body.split(/(?<=\n)/);
940
+ let offset = openLine.length;
941
+ for (const line of rest) {
942
+ offset += line.length;
943
+ if (fence.isClose(line.replace(/\r?\n$/, ""))) return body.slice(offset);
944
+ }
945
+ return body;
946
+ }
947
+ function formatForExtension(ext) {
948
+ switch (ext.toLowerCase()) {
949
+ case ".md":
950
+ case ".markdown":
951
+ return "markdown";
952
+ case ".mdx":
953
+ return "mdx";
954
+ default:
955
+ return "markdown";
956
+ }
957
+ }
958
+ function readPage(absPath, root) {
959
+ const content = readFileSync(absPath, "utf8");
960
+ const file = relative(root, absPath).replace(/\\/g, "/");
961
+ const format = formatForExtension(extname(absPath));
962
+ try {
963
+ const frontmatter = extractFrontmatter(content, format);
964
+ return {
965
+ file,
966
+ absPath,
967
+ content,
968
+ body: stripFrontmatterBlock(content),
969
+ frontmatter
970
+ };
971
+ } catch (e) {
972
+ return {
973
+ file,
974
+ absPath,
975
+ content,
976
+ body: stripFrontmatterBlock(content),
977
+ frontmatter: {
978
+ data: {},
979
+ present: false,
980
+ format,
981
+ lineFor: () => void 0
982
+ },
983
+ extractError: errorMessage(e)
984
+ };
985
+ }
986
+ }
987
+ var FAMILY_EXCLUDE = ["**/node_modules/**", "**/.git/**"];
988
+ function runConfigOptions(paths, options) {
989
+ return {
990
+ paths,
991
+ ...options.config === void 0 ? {} : { configPath: options.config },
992
+ ...options.noConfig === void 0 ? {} : { noConfig: options.noConfig },
993
+ ...options.collection === void 0 ? {} : { collection: options.collection }
994
+ };
995
+ }
996
+ function documentSet(paths, options, verb) {
997
+ return {
998
+ paths,
999
+ verb,
1000
+ ...options.collection === void 0 ? {} : { collection: options.collection },
1001
+ ...options.exclude === void 0 ? {} : { exclude: options.exclude }
1002
+ };
1003
+ }
1004
+ function toPatterns(input, base) {
1005
+ if (fg.isDynamicPattern(input)) return [input.replace(/\\/g, "/")];
1006
+ let isDirectory;
1007
+ try {
1008
+ isDirectory = statSync(resolve2(base, input)).isDirectory();
1009
+ } catch {
1010
+ return [input.replace(/\\/g, "/")];
1011
+ }
1012
+ const literal = fg.convertPathToPattern(input);
1013
+ return [isDirectory ? `${literal.replace(/\/$/, "")}/**/*` : literal];
1014
+ }
1015
+ function glob(patterns, base, ignore) {
1016
+ return fg.sync(patterns.flatMap((p) => toPatterns(p, base)), {
1017
+ cwd: base,
1018
+ ignore,
1019
+ absolute: true,
1020
+ dot: false,
1021
+ onlyFiles: true
1022
+ });
1023
+ }
1024
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
1025
+ function discoverPages(config, options = {}, root = process.cwd()) {
1026
+ const paths = options.paths ?? [];
1027
+ const wanted = options.collection ?? [];
1028
+ assertCollectionWithoutPaths(wanted, paths);
1029
+ if (wanted.length > 0 && config.configSource === null) {
1030
+ throw new DocevalsError("--collection needs a config file to select from.");
1031
+ }
1032
+ const collections = selectCollections(
1033
+ config.collections,
1034
+ wanted,
1035
+ config.configSource ?? DEFAULT_CONFIG_FILENAME,
1036
+ (message) => new DocevalsError(message)
1037
+ );
1038
+ const exclude = [...FAMILY_EXCLUDE, ...options.exclude ?? []];
1039
+ let entries;
1040
+ let patterns;
1041
+ if (paths.length > 0) {
1042
+ patterns = paths;
1043
+ entries = glob(paths, root, exclude);
1044
+ } else {
1045
+ patterns = collections.flatMap((c) => c.paths);
1046
+ if (patterns.length === 0) {
1047
+ throw new DocevalsError(
1048
+ `No files to ${options.verb ?? "evaluate"}. Pass paths/globs, or declare a collection under \`collections:\` in manni.config.yaml.`
1049
+ );
1050
+ }
1051
+ entries = collections.flatMap(
1052
+ (c) => glob(c.paths, config.configDir, [...exclude, ...c.exclude])
1053
+ );
1054
+ }
1055
+ const files = [
1056
+ ...new Set(entries.filter((p) => SUPPORTED_EXTENSIONS.has(extname(p).toLowerCase())))
1057
+ ];
1058
+ if (files.length === 0) {
1059
+ throw new DocevalsError(
1060
+ `No documentation pages found (patterns: ${patterns.join(", ")})`
1061
+ );
1062
+ }
1063
+ return files.sort().map((p) => readPage(resolve2(p), root));
1064
+ }
1065
+
1066
+ // src/docevals/core/external.ts
1067
+ import { resolve as resolve3 } from "node:path";
1068
+ var EVAL_KEYS = ["evals", "eval-suite", "eval-skip"];
1069
+ var EXTERNAL_KEYS = [
1070
+ ...EVAL_KEYS,
1071
+ "provenance",
1072
+ "meta-provenance"
1073
+ ];
1074
+ function urlManifestRefusal(source, collection, key) {
1075
+ return `${source}: collection ${collection}: ${key} cannot come from a URL manifest, because docevals writes them.`;
1076
+ }
1077
+ function twoManifestsRefusal(label, a, b, key) {
1078
+ return `${label} is in collections ${a} and ${b}, and both keep ${key} in a manifest.`;
1079
+ }
1080
+ var MARKED = new Set(EXTERNAL_KEYS);
1081
+ function owns(collection, manifest, key) {
1082
+ return ownsKey(collection, manifest, key, MARKED);
1083
+ }
1084
+ function evalKeysOf(collection, manifest) {
1085
+ return EVAL_KEYS.filter((k) => owns(collection, manifest, k));
1086
+ }
1087
+ async function loadExternalReader(config, base, pages) {
1088
+ const collections = [];
1089
+ const source = config.configSource ?? config.configPath;
1090
+ for (const collection of config.collections) {
1091
+ const owning = collection.externalMetadata.filter(
1092
+ (m) => EXTERNAL_KEYS.some((k) => owns(collection, m, k))
1093
+ );
1094
+ if (owning.length === 0) continue;
1095
+ for (const manifest of owning) {
1096
+ if (classifyRef(manifest.file).kind !== "url") continue;
1097
+ const [owned] = evalKeysOf(collection, manifest);
1098
+ if (owned !== void 0) {
1099
+ throw new DocevalsError(
1100
+ urlManifestRefusal(source, collection.name, owned)
1101
+ );
1102
+ }
1103
+ }
1104
+ collections.push({ ...collection, externalMetadata: owning });
1105
+ }
1106
+ if (collections.length === 0) return null;
1107
+ const index = await loadExternalMetadata(collections, {
1108
+ configDir: config.configDir,
1109
+ base,
1110
+ // A `{page}` entry names one manifest per page (proposal 0058), so it
1111
+ // reads a file only for the pages it is handed. Without them it reads
1112
+ // nothing, and every value the corpus keeps beside its pages goes missing
1113
+ // while the ownership that hides the page's own copy stays.
1114
+ ...pages === void 0 ? {} : { pages }
1115
+ });
1116
+ if (index === null) return null;
1117
+ return reader(index, collections, config.configDir, base, marksFor(config, base));
1118
+ }
1119
+ function marksFor(config, base) {
1120
+ return familyMarks({ configPath: config.configPath, configDir: config.configDir, cwd: base });
1121
+ }
1122
+ function ownersOf(collections, members, key) {
1123
+ return collections.filter(
1124
+ (c) => members.includes(c.name) && c.externalMetadata.some((m) => owns(c, m, key))
1125
+ ).map((c) => c.name);
1126
+ }
1127
+ function reader(index, collections, configDir, base, marks) {
1128
+ return {
1129
+ async forPage(page) {
1130
+ if (page.extractError !== void 0) return page;
1131
+ const members = memberOf(collections, configDir, base, page.file);
1132
+ if (members.length === 0) return page;
1133
+ for (const key of EVAL_KEYS) {
1134
+ const [a, b] = ownersOf(collections, members, key);
1135
+ if (a !== void 0 && b !== void 0) {
1136
+ throw new DocevalsError(twoManifestsRefusal(page.file, a, b, key));
1137
+ }
1138
+ }
1139
+ const merged = await mergeWithMarks(
1140
+ page.file,
1141
+ page.frontmatter,
1142
+ index,
1143
+ members,
1144
+ base,
1145
+ { marks: await marks() }
1146
+ );
1147
+ return {
1148
+ ...page,
1149
+ frontmatter: merged.extracted,
1150
+ external: {
1151
+ locate: merged.locate,
1152
+ // Only the keys this tool reads and writes. A manifest owning a
1153
+ // sibling tool's key that the page also carries is `meta validate`'s
1154
+ // finding, and saying it twice would make the same fix look like two.
1155
+ collisions: merged.collisions.filter(
1156
+ (c) => EVAL_KEYS.includes(c.key)
1157
+ ),
1158
+ evalManifests: suppliers(merged.locate, base)
1159
+ }
1160
+ };
1161
+ }
1162
+ };
1163
+ }
1164
+ function suppliers(locate, base) {
1165
+ const files = /* @__PURE__ */ new Set();
1166
+ for (const key of EVAL_KEYS) {
1167
+ const at = locate(`/${key}`);
1168
+ if (at !== void 0) files.add(resolve3(base, at.file));
1169
+ }
1170
+ return [...files];
1171
+ }
1172
+ async function withExternalMetadata(pages, config, base) {
1173
+ const merge = await loadExternalReader(
1174
+ config,
1175
+ base,
1176
+ pages.map((p) => p.absPath)
1177
+ );
1178
+ if (merge === null) return pages;
1179
+ return Promise.all(pages.map((p) => merge.forPage(p)));
1180
+ }
1181
+
1182
+ // src/docevals/core/resolve.ts
1183
+ import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
1184
+
1185
+ // src/docevals/schema.ts
1186
+ var frontmatterSchema = __default;
1187
+ var FRONTMATTER_SCHEMA_ID = __default.$id;
1188
+
1189
+ // src/docevals/graders/command.ts
1190
+ import { dirname as dirname2 } from "node:path";
1191
+ function substitute(cmd, file) {
1192
+ return cmd.map((part) => part.replaceAll("{file}", file));
1193
+ }
1194
+ async function gradeOne(ctx, target) {
1195
+ const { plan, eval: ev } = target;
1196
+ if (!ev.command) {
1197
+ return [
1198
+ {
1199
+ evalName: ev.name,
1200
+ file: plan.page.file,
1201
+ message: "No command to run (script not yet generated \u2014 run `manni docevals generate` or `manni docevals run` with a provider configured)",
1202
+ severity: ev.severity,
1203
+ diagnostic: true
1204
+ }
1205
+ ];
1206
+ }
1207
+ const cwd = ev.source === "page" ? dirname2(plan.page.absPath) : ctx.config.configDir;
1208
+ const result = await ctx.exec(substitute(ev.command, plan.page.absPath), {
1209
+ cwd,
1210
+ timeoutMs: ev.timeoutMs ?? ctx.config.scripts.timeoutMs,
1211
+ env: { MANNI_DOCEVALS_FILE: plan.page.absPath }
1212
+ });
1213
+ if (result.spawnError) {
1214
+ return [
1215
+ {
1216
+ evalName: ev.name,
1217
+ file: plan.page.file,
1218
+ message: `Failed to run command "${ev.command[0] ?? "(empty)"}": ${result.spawnError}`,
1219
+ severity: ev.severity,
1220
+ diagnostic: true
1221
+ }
1222
+ ];
1223
+ }
1224
+ if (result.timedOut) {
1225
+ return [
1226
+ {
1227
+ evalName: ev.name,
1228
+ file: plan.page.file,
1229
+ message: `Command timed out after ${ev.timeoutMs ?? ctx.config.scripts.timeoutMs}ms`,
1230
+ severity: ev.severity,
1231
+ diagnostic: true
1232
+ }
1233
+ ];
1234
+ }
1235
+ if (result.code != null && ev.successExitCodes.includes(result.code)) {
1236
+ return [];
1237
+ }
1238
+ const tail = outputTail(result);
1239
+ return [
1240
+ {
1241
+ evalName: ev.name,
1242
+ file: plan.page.file,
1243
+ // "Exit code N" is the message users and docs already know; only the
1244
+ // null case — killed by a signal, which is what a timeout produces —
1245
+ // needs different words, because "Exit code null" describes nothing.
1246
+ message: (result.code === null ? "Killed before exiting" : `Exit code ${result.code}`) + (tail ? `: ${tail}` : ""),
1247
+ severity: ev.severity
1248
+ }
1249
+ ];
1250
+ }
1251
+ var commandGrader = {
1252
+ kind: "command",
1253
+ mode: "per-file",
1254
+ async grade(ctx) {
1255
+ const findings = [];
1256
+ for (const target of ctx.targets) {
1257
+ findings.push(...await gradeOne(ctx, target));
1258
+ }
1259
+ return findings;
1260
+ }
1261
+ };
1262
+
1263
+ // src/docevals/graders/options.ts
1264
+ function firstError(...checks) {
1265
+ return checks.find((check) => check !== void 0);
1266
+ }
1267
+ function knownKeys(options, allowed) {
1268
+ const unknown = Object.keys(options).filter((k) => !allowed.includes(k));
1269
+ if (unknown.length === 0) return void 0;
1270
+ const listed = [...allowed].sort().join(", ");
1271
+ return `unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")} \u2014 this grader accepts: ${listed}`;
1272
+ }
1273
+ function requiredString(options, key) {
1274
+ const value = options[key];
1275
+ if (typeof value !== "string" || value.length === 0) {
1276
+ return `options.${key} is required`;
1277
+ }
1278
+ return void 0;
1279
+ }
1280
+ function optionalString(options, key) {
1281
+ const value = options[key];
1282
+ if (value === void 0) return void 0;
1283
+ if (typeof value !== "string" || value.length === 0) {
1284
+ return `options.${key} must be a non-empty string`;
1285
+ }
1286
+ return void 0;
1287
+ }
1288
+
1289
+ // src/docevals/core/target.ts
1290
+ import { readFileSync as readFileSync2 } from "node:fs";
1291
+ import { dirname as dirname3, isAbsolute, relative as relative2, resolve as resolve4, sep } from "node:path";
1292
+ import { stringify as stringifyYaml } from "yaml";
1293
+ var DEFAULT_TARGET = "body";
1294
+ function readTarget(target, plan) {
1295
+ const t = target ?? DEFAULT_TARGET;
1296
+ if (t === "body") return { ok: true, text: plan.page.body, label: "body" };
1297
+ if (t === "raw") return { ok: true, text: plan.page.content, label: "raw" };
1298
+ if (t === "frontmatter") {
1299
+ const data = plan.page.frontmatter.data;
1300
+ return {
1301
+ ok: true,
1302
+ text: stringifyYaml(data ?? {}),
1303
+ label: "frontmatter"
1304
+ };
1305
+ }
1306
+ const pageDir = dirname3(plan.page.absPath);
1307
+ if (isAbsolute(t.path)) {
1308
+ return {
1309
+ ok: false,
1310
+ reason: `target file "${t.path}" is an absolute path; name it relative to the page`
1311
+ };
1312
+ }
1313
+ const abs = resolve4(pageDir, t.path);
1314
+ const rel = relative2(pageDir, abs);
1315
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
1316
+ return {
1317
+ ok: false,
1318
+ reason: `target file "${t.path}" resolves outside the page's directory`
1319
+ };
1320
+ }
1321
+ try {
1322
+ return { ok: true, text: readFileSync2(abs, "utf8"), label: `file ${t.path}` };
1323
+ } catch (err) {
1324
+ const code = err instanceof Error ? err.code : void 0;
1325
+ return {
1326
+ ok: false,
1327
+ reason: `target file "${t.path}" could not be read (${code ?? "unknown error"})`
1328
+ };
1329
+ }
1330
+ }
1331
+
1332
+ // src/docevals/graders/native/regex.ts
1333
+ var MATCH_RE = /^(contains|not-contains|count:\d+)$/;
1334
+ var FLAGS_RE = /^[dgimsuvy]*$/;
1335
+ function lineAt(text, index) {
1336
+ let line = 1;
1337
+ for (let i = 0; i < index && i < text.length; i++) {
1338
+ if (text[i] === "\n") line++;
1339
+ }
1340
+ return line;
1341
+ }
1342
+ function linesAbove(target, page) {
1343
+ if (target !== void 0 && target !== "body") return 0;
1344
+ if (!page.content.endsWith(page.body)) return 0;
1345
+ return lineAt(page.content, page.content.length - page.body.length) - 1;
1346
+ }
1347
+ var regexGrader = {
1348
+ kind: "tool:regex",
1349
+ // It calls `readTarget` directly, so it serves the whole vocabulary — the
1350
+ // one deterministic grader that does. Declaring it here is what exempts it
1351
+ // from the engine's body-only guard (ADR 01033).
1352
+ targets: ["body", "raw", "frontmatter", "file"],
1353
+ validateOptions(options) {
1354
+ const match = options.match;
1355
+ return firstError(
1356
+ knownKeys(options, ["pattern", "flags", "match"]),
1357
+ requiredString(options, "pattern"),
1358
+ optionalString(options, "flags"),
1359
+ typeof options.flags === "string" && !FLAGS_RE.test(options.flags) ? "options.flags must be JS RegExp flags (d g i m s u v y)" : void 0,
1360
+ match !== void 0 && (typeof match !== "string" || !MATCH_RE.test(match)) ? "options.match must be contains, not-contains, or count:N" : void 0,
1361
+ // A pattern that will not compile is a typo, not a finding: reporting it
1362
+ // as a failed assertion would blame the page for the eval's own bug.
1363
+ (() => {
1364
+ if (typeof options.pattern !== "string") return void 0;
1365
+ try {
1366
+ new RegExp(options.pattern, options.flags ?? "");
1367
+ return void 0;
1368
+ } catch (err) {
1369
+ return `options.pattern is not a valid regular expression (${errorMessage(err)})`;
1370
+ }
1371
+ })()
1372
+ );
1373
+ },
1374
+ mode: "per-file",
1375
+ grade(ctx) {
1376
+ return Promise.resolve().then(() => {
1377
+ const findings = [];
1378
+ for (const { plan, eval: ev } of ctx.targets) {
1379
+ const opts = ev.options;
1380
+ const pattern = opts.pattern ?? "";
1381
+ const flags = opts.flags ?? "";
1382
+ const match = opts.match ?? "contains";
1383
+ const selected = readTarget(ev.target, plan);
1384
+ if (!selected.ok) {
1385
+ findings.push({
1386
+ evalName: ev.name,
1387
+ file: plan.page.file,
1388
+ ruleId: "regex/unreadable-target",
1389
+ message: selected.reason,
1390
+ severity: ev.severity,
1391
+ line: 1
1392
+ });
1393
+ continue;
1394
+ }
1395
+ const text = selected.text;
1396
+ const offset = linesAbove(ev.target, plan.page);
1397
+ const all = [...text.matchAll(new RegExp(pattern, flags.includes("g") ? flags : `${flags}g`))];
1398
+ const first = all[0];
1399
+ if (match === "contains" && all.length === 0) {
1400
+ findings.push({
1401
+ evalName: ev.name,
1402
+ file: plan.page.file,
1403
+ ruleId: "regex/not-found",
1404
+ message: `Pattern /${pattern}/${flags} not found in ${selected.label}`,
1405
+ severity: ev.severity,
1406
+ line: 1
1407
+ });
1408
+ } else if (match === "not-contains" && first !== void 0) {
1409
+ findings.push({
1410
+ evalName: ev.name,
1411
+ file: plan.page.file,
1412
+ ruleId: "regex/found",
1413
+ message: `Pattern /${pattern}/${flags} found in ${selected.label}, expected absent`,
1414
+ severity: ev.severity,
1415
+ line: offset + lineAt(text, first.index)
1416
+ });
1417
+ } else if (match.startsWith("count:")) {
1418
+ const want = Number(match.slice("count:".length));
1419
+ if (all.length !== want) {
1420
+ findings.push({
1421
+ evalName: ev.name,
1422
+ file: plan.page.file,
1423
+ ruleId: "regex/count",
1424
+ message: `Pattern /${pattern}/${flags} matched ${String(all.length)} time(s) in ${selected.label}, expected ${String(want)}`,
1425
+ severity: ev.severity,
1426
+ line: first === void 0 ? 1 : offset + lineAt(text, first.index)
1427
+ });
1428
+ }
1429
+ }
1430
+ }
1431
+ return findings;
1432
+ });
1433
+ }
1434
+ };
1435
+
1436
+ // src/docevals/graders/registry.ts
1437
+ var GRADERS = new Map(
1438
+ [commandGrader, regexGrader].map((g) => [g.kind, g])
1439
+ );
1440
+ var UNMAPPED_KINDS = ["ai", "human"];
1441
+ var BUILT_IN_ORDER = ["ai", "command", "human", "tool:regex"];
1442
+ function graderFor(kind) {
1443
+ return GRADERS.get(kind);
1444
+ }
1445
+ function listGraderKinds() {
1446
+ return [.../* @__PURE__ */ new Set([...BUILT_IN_ORDER, ...GRADERS.keys()])];
1447
+ }
1448
+ function isRegisteredGrader(kind) {
1449
+ return UNMAPPED_KINDS.includes(kind) || GRADERS.has(kind);
1450
+ }
1451
+ function registerGrader(grader) {
1452
+ GRADERS.set(grader.kind, grader);
1453
+ }
1454
+
1455
+ // src/docevals/core/resolve.ts
1456
+ var ajv2 = new Ajv20202({ allErrors: true, allowUnionTypes: true, strict: false });
1457
+ var validateFrontmatter = ajv2.compile(frontmatterSchema);
1458
+ function evalEntries(raw) {
1459
+ if (raw === void 0) return [];
1460
+ return typeof raw === "string" ? [raw] : raw;
1461
+ }
1462
+ function fromDef(name, suite, def, source, location) {
1463
+ return {
1464
+ location,
1465
+ name,
1466
+ suite,
1467
+ assertion: def.assertion,
1468
+ type: def.type ?? "regression",
1469
+ grader: def.grader ?? "ai",
1470
+ provider: def.provider,
1471
+ evidence: def.evidence,
1472
+ examples: def.examples,
1473
+ command: def.command,
1474
+ successExitCodes: def.successExitCodes ?? [0],
1475
+ timeoutMs: def.timeoutMs,
1476
+ generatedAssertionHash: def.generatedAssertionHash,
1477
+ options: def.options ?? {},
1478
+ severity: def.severity ?? "error",
1479
+ weight: def.weight ?? 1,
1480
+ target: def.target,
1481
+ model: def.model,
1482
+ runs: def.runs,
1483
+ source,
1484
+ skip: false
1485
+ };
1486
+ }
1487
+ function shorthandName(index, taken) {
1488
+ const base = `assertion-${index + 1}`;
1489
+ if (!taken.has(base)) return base;
1490
+ for (let n = 2; ; n++) {
1491
+ const candidate = `${base}-${n}`;
1492
+ if (!taken.has(candidate)) return candidate;
1493
+ }
1494
+ }
1495
+ var RESERVED_KEYS = ["eval-suite", "eval-skip"];
1496
+ function describeError(e) {
1497
+ if (e.keyword === "false schema" && e.instancePath.startsWith("/eval-")) {
1498
+ const key = e.instancePath.slice(1);
1499
+ return `unknown key "${key}". The "eval-" prefix is reserved, and the only settings under it are ${RESERVED_KEYS.join(", ")} \u2014 so a typo is an error here rather than a key nothing reads.`;
1500
+ }
1501
+ return e.message ?? "is invalid";
1502
+ }
1503
+ function locationOf(page, pointer) {
1504
+ const at = declaredAt(page.file, pointer, metadataOf(page));
1505
+ return at.file === page.file ? { line: at.line ?? 1 } : at;
1506
+ }
1507
+ function metadataOf(page) {
1508
+ return {
1509
+ extracted: page.frontmatter,
1510
+ ...page.external === void 0 ? {} : { locate: page.external.locate }
1511
+ };
1512
+ }
1513
+ function declaredLocation(page, pointer, key) {
1514
+ if (!(key in page.frontmatter.data)) return { file: page.file };
1515
+ return declaredAt(page.file, pointer, metadataOf(page));
1516
+ }
1517
+ function resolvePage(page, config) {
1518
+ const problems = [];
1519
+ const empty = {
1520
+ page,
1521
+ skip: false,
1522
+ suite: null,
1523
+ evals: [],
1524
+ problems
1525
+ };
1526
+ if (page.extractError) {
1527
+ problems.push({ message: page.extractError, level: "error", line: 1 });
1528
+ return empty;
1529
+ }
1530
+ for (const c of page.external?.collisions ?? []) {
1531
+ problems.push({
1532
+ message: `"${c.key}" is owned by manifest ${c.file} (collection ${c.collection}); remove it from the document`,
1533
+ level: "error",
1534
+ line: page.frontmatter.lineFor(`/${c.key}`) ?? 1
1535
+ });
1536
+ }
1537
+ const data = page.frontmatter.data;
1538
+ if (!validateFrontmatter(data)) {
1539
+ for (const e of validateFrontmatter.errors ?? []) {
1540
+ problems.push({
1541
+ message: `frontmatter${e.instancePath}: ${describeError(e)}`,
1542
+ level: "error",
1543
+ ...locationOf(page, e.instancePath)
1544
+ });
1545
+ }
1546
+ return empty;
1547
+ }
1548
+ const fm = data;
1549
+ const pageSkip = fm["eval-skip"] ?? false;
1550
+ const declaredSuite = fm["eval-suite"];
1551
+ const suiteName = declaredSuite ?? config.defaults.suite;
1552
+ if (declaredSuite && !(declaredSuite in config.suites)) {
1553
+ problems.push({
1554
+ message: `Unknown suite "${declaredSuite}" (not defined in ${config.configPath})`,
1555
+ level: "error",
1556
+ ...locationOf(page, "/eval-suite")
1557
+ });
1558
+ return { ...empty, skip: pageSkip };
1559
+ }
1560
+ const resolved = /* @__PURE__ */ new Map();
1561
+ if (suiteName) {
1562
+ const suite = config.suites[suiteName];
1563
+ const attached = declaredLocation(page, "/eval-suite", "eval-suite");
1564
+ for (const name of suite?.evals ?? []) {
1565
+ const def = config.evals[name];
1566
+ if (def) resolved.set(name, fromDef(name, suiteName, def, "config", attached));
1567
+ }
1568
+ }
1569
+ const reportSuite = suiteName ?? "default";
1570
+ const claimed = new Set(resolved.keys());
1571
+ for (const entry of evalEntries(fm.evals)) {
1572
+ if (typeof entry === "string") continue;
1573
+ claimed.add("use" in entry ? entry.use : entry.id);
1574
+ }
1575
+ for (const [i, entry] of evalEntries(fm.evals).entries()) {
1576
+ const linePtr = `/evals/${String(i)}`;
1577
+ const entryAt = declaredLocation(page, linePtr, "evals");
1578
+ if (typeof entry === "string") {
1579
+ if (entry in config.evals) {
1580
+ problems.push({
1581
+ message: `String shorthand "${entry}" matches an eval defined in ${config.configPath}, but a string is an assertion now, not a reference. Write "use: ${entry}" to run that eval.`,
1582
+ level: "warning",
1583
+ ...locationOf(page, linePtr)
1584
+ });
1585
+ }
1586
+ const name = shorthandName(i, claimed);
1587
+ claimed.add(name);
1588
+ resolved.set(
1589
+ name,
1590
+ fromDef(name, reportSuite, { assertion: entry }, "page", entryAt)
1591
+ );
1592
+ continue;
1593
+ }
1594
+ if ("use" in entry) {
1595
+ const ref = entry;
1596
+ const def = config.evals[ref.use];
1597
+ if (!def) {
1598
+ problems.push({
1599
+ message: `Unknown eval "${ref.use}" (not defined in ${config.configPath})`,
1600
+ level: "error",
1601
+ ...locationOf(page, linePtr)
1602
+ });
1603
+ continue;
1604
+ }
1605
+ const base = resolved.get(ref.use) ?? fromDef(ref.use, reportSuite, def, "config", entryAt);
1606
+ resolved.set(ref.use, {
1607
+ ...base,
1608
+ location: entryAt,
1609
+ type: ref.type ?? base.type,
1610
+ severity: ref.severity ?? base.severity,
1611
+ weight: ref.weight ?? base.weight,
1612
+ options: { ...base.options, ...ref.options ?? {} },
1613
+ skip: ref.skip ?? base.skip
1614
+ });
1615
+ continue;
1616
+ }
1617
+ const inline = entry;
1618
+ if (resolved.has(inline.id) && resolved.get(inline.id)?.source === "page") {
1619
+ problems.push({
1620
+ message: `Duplicate eval id "${inline.id}" on page \u2014 the later entry replaces the earlier one`,
1621
+ level: "error",
1622
+ ...locationOf(page, linePtr)
1623
+ });
1624
+ }
1625
+ const ev = fromDef(inline.id, reportSuite, normalizeEvalDef(inline), "page", entryAt);
1626
+ ev.skip = inline.skip ?? false;
1627
+ resolved.set(inline.id, ev);
1628
+ if ("severity-map" in inline && isRegisteredGrader(ev.grader)) {
1629
+ warn(
1630
+ `${page.file}: eval "${inline.id}" sets severity-map, which no registered grader reads; it has no effect.`
1631
+ );
1632
+ }
1633
+ if (ev.grader === "ai" && !inline.examples) {
1634
+ problems.push({
1635
+ message: `Eval "${inline.id}": ai-graded evals work best with examples.pass/examples.fail`,
1636
+ level: "warning",
1637
+ ...locationOf(page, linePtr)
1638
+ });
1639
+ }
1640
+ }
1641
+ return {
1642
+ page,
1643
+ skip: pageSkip,
1644
+ suite: suiteName,
1645
+ evals: [...resolved.values()],
1646
+ problems
1647
+ };
1648
+ }
1649
+ function resolvePages(pages, config) {
1650
+ return pages.map((p) => resolvePage(p, config));
1651
+ }
1652
+
1653
+ // src/docevals/core/feasibility.ts
1654
+ function unregistered(where, name, grader) {
1655
+ return new DocevalsError(
1656
+ `${where}: eval "${name}" names grader "${grader}", which is not registered. Registered graders: ${listGraderKinds().join(", ")}.`
1657
+ );
1658
+ }
1659
+ function assertRegisteredGraders(plans, config) {
1660
+ const configFile = config.configSource ?? config.configPath;
1661
+ for (const [name, def] of Object.entries(config.evals)) {
1662
+ const grader = def.grader ?? "ai";
1663
+ if (!isRegisteredGrader(grader)) throw unregistered(configFile, name, grader);
1664
+ }
1665
+ for (const plan of plans) {
1666
+ for (const ev of plan.evals) {
1667
+ if (ev.source === "page" && !isRegisteredGrader(ev.grader)) {
1668
+ throw unregistered(plan.page.file, ev.name, ev.grader);
1669
+ }
1670
+ }
1671
+ }
1672
+ }
1673
+ function checkFeasibility(plans, options) {
1674
+ const problems = [];
1675
+ for (const plan of plans) {
1676
+ if (plan.skip) continue;
1677
+ for (const ev of plan.evals) {
1678
+ if (ev.skip) continue;
1679
+ const isAi = ev.grader === "ai";
1680
+ if (isAi && options.deterministicOnly === true) continue;
1681
+ if (!isAi && ev.grader !== "human" && options.aiOnly === true) continue;
1682
+ const where = `Eval "${ev.name}"`;
1683
+ const grader = graderFor(ev.grader);
1684
+ const invalid = grader?.validateOptions?.(ev.options);
1685
+ if (invalid !== void 0) {
1686
+ problems.push({
1687
+ file: plan.page.file,
1688
+ level: "error",
1689
+ message: `${where}: ${ev.grader} ${invalid}`
1690
+ });
1691
+ }
1692
+ if (ev.grader === "command" && ev.source === "page" && !options.allowFrontmatterCommands && !ev.command && !options.canGenerate) {
1693
+ problems.push({
1694
+ file: plan.page.file,
1695
+ level: "error",
1696
+ message: `${where}: a page-declared command eval with no command, while frontmatter commands are not granted (execution.allow: [frontmatter-commands]) and generation is off \u2014 it can only ever be skipped`
1697
+ });
1698
+ }
1699
+ }
1700
+ }
1701
+ return problems;
1702
+ }
1703
+
1704
+ // src/docevals/core/baseline.ts
1705
+ import { createHash } from "node:crypto";
1706
+ import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
1707
+ import { dirname as dirname4, relative as relative3, resolve as resolve5 } from "node:path";
1708
+ var DEFAULT_BASELINE_PATH = ".manni-docevals-baseline.json";
1709
+ var BASELINE_VERSION = 1;
1710
+ var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
1711
+ function canonicalFilePath(file, ctx) {
1712
+ if (!ctx) return file;
1713
+ const from = ctx.runBase ?? ctx.base;
1714
+ return relative3(ctx.base, resolve5(from, file)).replace(/\\/g, "/");
1715
+ }
1716
+ function fingerprint(f) {
1717
+ const parts = [f.evalName, f.ruleId ?? ""];
1718
+ return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
1719
+ }
1720
+ function serializeBaseline(baseline) {
1721
+ const entries = /* @__PURE__ */ Object.create(null);
1722
+ for (const file of Object.keys(baseline.entries).sort()) {
1723
+ entries[file] = [...baseline.entries[file] ?? []].sort();
1724
+ }
1725
+ return `${JSON.stringify(
1726
+ { version: baseline.version, generatedWith: baseline.generatedWith, entries },
1727
+ null,
1728
+ 2
1729
+ )}
1730
+ `;
1731
+ }
1732
+ function bad(source, detail) {
1733
+ throw new DocevalsError(`Baseline "${source}": ${detail}`);
1734
+ }
1735
+ function parseBaseline(text, source) {
1736
+ let raw;
1737
+ try {
1738
+ raw = JSON.parse(text.replace(/^/, ""));
1739
+ } catch (err) {
1740
+ bad(source, `invalid JSON: ${errorMessage(err)}`);
1741
+ }
1742
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1743
+ bad(source, "top level must be an object.");
1744
+ }
1745
+ const obj = raw;
1746
+ if (obj.version !== BASELINE_VERSION) {
1747
+ bad(
1748
+ source,
1749
+ `unsupported version ${JSON.stringify(obj.version)} (this manni docevals writes version ${BASELINE_VERSION}). Re-record it with \`manni docevals run --write-baseline\`.`
1750
+ );
1751
+ }
1752
+ const rawEntries = obj.entries;
1753
+ if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
1754
+ bad(source, '"entries" must be an object mapping file paths to fingerprints.');
1755
+ }
1756
+ const entries = /* @__PURE__ */ Object.create(null);
1757
+ for (const [file, value] of Object.entries(rawEntries)) {
1758
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
1759
+ bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
1760
+ }
1761
+ const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
1762
+ if (malformed !== void 0) {
1763
+ bad(
1764
+ source,
1765
+ `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
1766
+ );
1767
+ }
1768
+ entries[file] = value;
1769
+ }
1770
+ return {
1771
+ version: BASELINE_VERSION,
1772
+ generatedWith: typeof obj.generatedWith === "string" ? obj.generatedWith : "",
1773
+ entries
1774
+ };
1775
+ }
1776
+ function readBaseline(absPath, source) {
1777
+ let text;
1778
+ try {
1779
+ text = readFileSync3(absPath, "utf8");
1780
+ } catch (err) {
1781
+ if (err.code === "ENOENT") return null;
1782
+ throw new DocevalsError(
1783
+ `Baseline "${source}" could not be read: ${errorMessage(err)}`
1784
+ );
1785
+ }
1786
+ return parseBaseline(text, source);
1787
+ }
1788
+ function writeBaselineFile(absPath, baseline, source) {
1789
+ try {
1790
+ mkdirSync(dirname4(absPath), { recursive: true });
1791
+ const tmp = `${absPath}.tmp`;
1792
+ writeFileSync(tmp, serializeBaseline(baseline));
1793
+ renameSync(tmp, absPath);
1794
+ } catch (err) {
1795
+ throw new DocevalsError(
1796
+ `Baseline "${source}" could not be written: ${errorMessage(err)}`
1797
+ );
1798
+ }
1799
+ }
1800
+ function buildBaseline(results, generatedWith, ctx) {
1801
+ const entries = /* @__PURE__ */ Object.create(null);
1802
+ for (const r of results) {
1803
+ const key = canonicalFilePath(r.file, ctx);
1804
+ for (const f of r.findings ?? []) {
1805
+ if (f.diagnostic === true) continue;
1806
+ const list = entries[key] ?? [];
1807
+ const print = fingerprint(f);
1808
+ if (!list.includes(print)) list.push(print);
1809
+ entries[key] = list;
1810
+ }
1811
+ }
1812
+ return { version: BASELINE_VERSION, generatedWith, entries };
1813
+ }
1814
+ function applyBaseline(results, baseline, ctx) {
1815
+ let suppressed = 0;
1816
+ const seenByFile = /* @__PURE__ */ new Map();
1817
+ const touched = /* @__PURE__ */ new Set();
1818
+ const applied = results.map((r) => {
1819
+ const key = canonicalFilePath(r.file, ctx);
1820
+ if (r.outcome === "pass" || r.outcome === "fail") touched.add(key);
1821
+ const findings = r.findings;
1822
+ if (!findings || findings.length === 0) return r;
1823
+ const known = baseline.entries[key];
1824
+ if (!known) return r;
1825
+ const recordedHere = new Set(known);
1826
+ const seen = seenByFile.get(key) ?? /* @__PURE__ */ new Set();
1827
+ const fresh = [];
1828
+ for (const f of findings) {
1829
+ const print = fingerprint(f);
1830
+ if (recordedHere.has(print)) {
1831
+ seen.add(print);
1832
+ suppressed += 1;
1833
+ } else {
1834
+ fresh.push(f);
1835
+ }
1836
+ }
1837
+ seenByFile.set(key, seen);
1838
+ if (fresh.length === findings.length) return r;
1839
+ const hasError = fresh.some(
1840
+ (f) => f.severity === "error" || f.diagnostic === true
1841
+ );
1842
+ return {
1843
+ ...r,
1844
+ outcome: hasError ? "fail" : "pass",
1845
+ ...fresh.length > 0 ? { findings: fresh } : { findings: void 0 },
1846
+ baselined: findings.length - fresh.length
1847
+ };
1848
+ });
1849
+ let recorded = 0;
1850
+ let stale = 0;
1851
+ for (const key of touched) {
1852
+ const known = baseline.entries[key];
1853
+ if (!known) continue;
1854
+ const recordedHere = new Set(known);
1855
+ recorded += recordedHere.size;
1856
+ stale += recordedHere.size - (seenByFile.get(key)?.size ?? 0);
1857
+ }
1858
+ return { results: applied, recorded, suppressed, stale };
1859
+ }
1860
+ function diffBaselines(previous, next) {
1861
+ const flatten = (b) => {
1862
+ const out = /* @__PURE__ */ new Set();
1863
+ if (!b) return out;
1864
+ for (const [file, prints] of Object.entries(b.entries)) {
1865
+ for (const p of prints) out.add(`${file}\0${p}`);
1866
+ }
1867
+ return out;
1868
+ };
1869
+ const before = flatten(previous);
1870
+ const after = flatten(next);
1871
+ let added = 0;
1872
+ let removed = 0;
1873
+ for (const k of after) if (!before.has(k)) added += 1;
1874
+ for (const k of before) if (!after.has(k)) removed += 1;
1875
+ return { added, removed };
1876
+ }
1877
+ function countFingerprints(baseline) {
1878
+ let n = 0;
1879
+ for (const prints of Object.values(baseline.entries)) n += prints.length;
1880
+ return n;
1881
+ }
1882
+
1883
+ // src/docevals/core/since.ts
1884
+ import { resolve as resolve6 } from "node:path";
1885
+ var GIT_TIMEOUT_MS = 3e4;
1886
+ function changedKey(absPath) {
1887
+ const abs = resolve6(absPath);
1888
+ return process.platform === "win32" ? abs.toLowerCase() : abs;
1889
+ }
1890
+ function stderrOf(result) {
1891
+ return outputTail(result) || "(no output)";
1892
+ }
1893
+ async function git(exec, args, root) {
1894
+ const result = await exec(args, { cwd: root, timeoutMs: GIT_TIMEOUT_MS });
1895
+ if (result.spawnError) {
1896
+ throw new DocevalsError(
1897
+ `--since needs git on PATH: could not run git (${result.spawnError}). Install git, or drop --since to evaluate the whole corpus.`
1898
+ );
1899
+ }
1900
+ if (result.timedOut) {
1901
+ throw new DocevalsError(
1902
+ `--since timed out: \`${args.join(" ")}\` took longer than ${GIT_TIMEOUT_MS / 1e3}s in ${root}.`
1903
+ );
1904
+ }
1905
+ return result;
1906
+ }
1907
+ async function changedFilesSince(ref, root, exec) {
1908
+ if (ref.startsWith("-")) {
1909
+ throw new DocevalsError(
1910
+ `--since "${ref}" is not a ref: a value starting with "-" is read by git as an option, which would silently produce an empty diff.`
1911
+ );
1912
+ }
1913
+ if (ref.trim() === "") {
1914
+ throw new DocevalsError(
1915
+ `--since was given an empty ref. Git would read that as HEAD and scope the run to nothing, which exits 0 having checked nothing. If this came from a CI expression, the variable is unset on this event.`
1916
+ );
1917
+ }
1918
+ const top = await git(exec, ["git", "rev-parse", "--show-toplevel"], root);
1919
+ if (top.code !== 0) {
1920
+ throw new DocevalsError(
1921
+ `--since requires a git repository, and ${root} is not a git repository (or git cannot read it). git said: ${stderrOf(top)}`
1922
+ );
1923
+ }
1924
+ const topLevel = top.stdout.trim();
1925
+ const diff = await git(
1926
+ exec,
1927
+ ["git", "--no-pager", "diff", "--name-only", "-z", `${ref}...HEAD`],
1928
+ root
1929
+ );
1930
+ if (diff.code !== 0) {
1931
+ throw new DocevalsError(
1932
+ `--since ${ref}: git could not resolve "${ref}...HEAD". In CI this is usually a shallow clone \u2014 actions/checkout defaults to fetch-depth: 1, which fetches no other branch, so set fetch-depth: 0 (or fetch ${ref} explicitly) before using --since. git said: ${stderrOf(diff)}`
1933
+ );
1934
+ }
1935
+ const changed = /* @__PURE__ */ new Set();
1936
+ for (const rel of diff.stdout.split("\0")) {
1937
+ if (rel === "") continue;
1938
+ changed.add(changedKey(resolve6(topLevel, rel)));
1939
+ }
1940
+ return changed;
1941
+ }
1942
+
1943
+ // src/docevals/core/newer-than.ts
1944
+ import { statSync as statSync2 } from "node:fs";
1945
+ import { relative as relative4, resolve as resolve7 } from "node:path";
1946
+ var GIT_TIMEOUT_MS2 = 3e4;
1947
+ async function git2(exec, args, cwd) {
1948
+ const result = await exec(args, { cwd, timeoutMs: GIT_TIMEOUT_MS2 });
1949
+ if (result.timedOut) {
1950
+ throw new DocevalsError(
1951
+ `--newer-than timed out: \`${args.join(" ")}\` took longer than ${String(GIT_TIMEOUT_MS2 / 1e3)}s in ${cwd}.`
1952
+ );
1953
+ }
1954
+ return result;
1955
+ }
1956
+ function keysOf(stdout, topLevel) {
1957
+ const keys = /* @__PURE__ */ new Set();
1958
+ for (const rel of stdout.split("\0")) {
1959
+ if (rel !== "") keys.add(changedKey(resolve7(topLevel, rel)));
1960
+ }
1961
+ return keys;
1962
+ }
1963
+ async function repoState(root, exec) {
1964
+ const top = await git2(exec, ["git", "rev-parse", "--show-toplevel"], root);
1965
+ if (top.spawnError !== void 0 || top.code !== 0) return null;
1966
+ const topLevel = top.stdout.trim();
1967
+ if (topLevel === "") return null;
1968
+ const tracked = await git2(exec, ["git", "ls-files", "-z", "--full-name"], topLevel);
1969
+ if (tracked.code !== 0) return null;
1970
+ const diff = await git2(
1971
+ exec,
1972
+ ["git", "--no-pager", "diff", "--name-only", "-z", "HEAD"],
1973
+ topLevel
1974
+ );
1975
+ const trackedKeys = keysOf(tracked.stdout, topLevel);
1976
+ return {
1977
+ topLevel,
1978
+ tracked: trackedKeys,
1979
+ modified: diff.code === 0 ? keysOf(diff.stdout, topLevel) : trackedKeys
1980
+ };
1981
+ }
1982
+ function newestMtime(files) {
1983
+ let newest = Number.NEGATIVE_INFINITY;
1984
+ for (const file of files) {
1985
+ try {
1986
+ newest = Math.max(newest, statSync2(file).mtimeMs);
1987
+ } catch {
1988
+ }
1989
+ }
1990
+ return newest;
1991
+ }
1992
+ async function lastCommit(files, repo, exec) {
1993
+ const paths = files.map((f) => relative4(repo.topLevel, f).replace(/\\/g, "/"));
1994
+ const log = await git2(
1995
+ exec,
1996
+ ["git", "--no-pager", "log", "-1", "--format=%ct", "--", ...paths],
1997
+ repo.topLevel
1998
+ );
1999
+ const seconds = Number.parseInt(log.stdout.trim(), 10);
2000
+ return log.code === 0 && Number.isFinite(seconds) ? seconds * 1e3 : void 0;
2001
+ }
2002
+ async function pageAges(pages, root, exec) {
2003
+ const repo = await repoState(root, exec);
2004
+ const ages = [];
2005
+ for (const { page, manifests } of pages) {
2006
+ const files = [page, ...manifests];
2007
+ const committed = repo !== null && files.every((f) => {
2008
+ const key = changedKey(f);
2009
+ return repo.tracked.has(key) && !repo.modified.has(key);
2010
+ });
2011
+ const fromGit = committed ? await lastCommit(files, repo, exec) : void 0;
2012
+ ages.push(fromGit ?? newestMtime(files));
2013
+ }
2014
+ return ages;
2015
+ }
2016
+
2017
+ // src/docevals/graders/types.ts
2018
+ function sortedForKey(value) {
2019
+ if (value === void 0 || value === null) return value;
2020
+ if (Array.isArray(value)) return value.map(sortedForKey);
2021
+ if (typeof value !== "object") return value;
2022
+ const entries = value;
2023
+ const out = {};
2024
+ for (const k of Object.keys(entries).sort()) {
2025
+ out[k] = sortedForKey(entries[k]);
2026
+ }
2027
+ return out;
2028
+ }
2029
+ function groupTargetsByEval(targets) {
2030
+ const groups = /* @__PURE__ */ new Map();
2031
+ for (const t of targets) {
2032
+ const key = JSON.stringify([
2033
+ t.eval.name,
2034
+ // Key order, not just content: `resolve.ts` rebuilds `options` per page
2035
+ // by spread, so insertion order follows each page's own YAML. Two pages
2036
+ // declaring the same options in a different order used to hash
2037
+ // differently and land in separate groups, so a batch grader made one
2038
+ // invocation per page instead of one per configuration. Sorting makes
2039
+ // the key describe the configuration rather than how it was typed.
2040
+ sortedForKey(t.eval.options),
2041
+ t.eval.timeoutMs ?? null,
2042
+ t.eval.severity
2043
+ ]);
2044
+ const list = groups.get(key) ?? [];
2045
+ list.push(t);
2046
+ groups.set(key, list);
2047
+ }
2048
+ return [...groups.values()];
2049
+ }
2050
+
2051
+ // src/docevals/judge/cache.ts
2052
+ import { buildCacheKey, JsonCache, sha256 } from "@hawkeyexl/inference";
2053
+
2054
+ // src/docevals/judge/prompt.ts
2055
+ var PROMPT_VERSION = 4;
2056
+ var JUDGE_SYSTEM_PROMPT = [
2057
+ "You are a meticulous technical documentation judge. You evaluate whether a",
2058
+ "documentation page satisfies a specific assertion.",
2059
+ "",
2060
+ "Rules:",
2061
+ "- Judge ONLY the stated assertion against the supplied page content. Do not",
2062
+ " invent requirements the assertion does not state.",
2063
+ '- "match" is "pass" only when the assertion is fully satisfied by the page.',
2064
+ '- Use "partial" when the page partially satisfies the assertion.',
2065
+ '- Use "fail" when the page does not satisfy the assertion.',
2066
+ '- Quote the specific page text you relied on in "observed". If the page',
2067
+ " lacks relevant content, say so explicitly.",
2068
+ "- Be conservative with confidence: reserve values above 0.9 for verdicts a",
2069
+ " careful human reviewer would certainly agree with.",
2070
+ "Respond with a JSON object matching the provided schema."
2071
+ ].join("\n");
2072
+ function cleanBody(body) {
2073
+ const out = [];
2074
+ let fence = null;
2075
+ for (const line of body.split("\n")) {
2076
+ const marker = /^(\s*)(```+|~~~+)/.exec(line)?.[2];
2077
+ if (fence) {
2078
+ out.push(line);
2079
+ if (marker?.startsWith(fence)) fence = null;
2080
+ continue;
2081
+ }
2082
+ if (marker !== void 0) {
2083
+ fence = marker;
2084
+ out.push(line);
2085
+ continue;
2086
+ }
2087
+ out.push(
2088
+ line.replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<!--[\s\S]*?-->/g, "").replace(/^(import|export)\s.*$/, "")
2089
+ );
2090
+ }
2091
+ return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
2092
+ }
2093
+ function buildUserContent(ev, body, targetLabel = "body") {
2094
+ const parts = [
2095
+ `# Assertion`,
2096
+ ev.assertion ?? ""
2097
+ ];
2098
+ if (ev.evidence) {
2099
+ parts.push("", `# Where to look`, ev.evidence);
2100
+ }
2101
+ if (ev.examples?.pass || ev.examples?.fail) {
2102
+ parts.push("", "# Anchors");
2103
+ for (const anchor of ev.examples.pass ?? []) {
2104
+ parts.push(`A passing page: ${anchor}`);
2105
+ }
2106
+ for (const anchor of ev.examples.fail ?? []) {
2107
+ parts.push(`A failing page: ${anchor}`);
2108
+ }
2109
+ }
2110
+ const isBody = targetLabel === "body";
2111
+ parts.push(
2112
+ "",
2113
+ isBody ? "# Page content" : `# Page content (${targetLabel})`,
2114
+ "",
2115
+ isBody ? cleanBody(body) : body
2116
+ );
2117
+ return parts.join("\n");
2118
+ }
2119
+ var EVIDENCE_SYSTEM_PROMPT = [
2120
+ "You are gathering evidence from one part of a documentation page so that a",
2121
+ "judge can later evaluate an assertion about the whole page.",
2122
+ "",
2123
+ "Rules:",
2124
+ "- Do NOT decide whether the assertion holds. You are seeing one part; the",
2125
+ " part that settles it may be elsewhere.",
2126
+ "- Quote page text verbatim. Quote anything that supports the assertion and",
2127
+ " anything that contradicts it.",
2128
+ "- Quote only what bears on this assertion. An empty list is the right",
2129
+ " answer for a part that says nothing about it.",
2130
+ "Respond with a JSON object matching the provided schema."
2131
+ ].join("\n");
2132
+ var EVIDENCE_SCHEMA = {
2133
+ type: "object",
2134
+ required: ["supports", "contradicts"],
2135
+ properties: {
2136
+ supports: { type: "array", items: { type: "string" } },
2137
+ contradicts: { type: "array", items: { type: "string" } }
2138
+ },
2139
+ additionalProperties: false
2140
+ };
2141
+ function buildEvidenceUser(ev, chunk, part) {
2142
+ return [
2143
+ "# Assertion (do not judge it \u2014 only gather evidence)",
2144
+ ev.assertion ?? "",
2145
+ ...ev.evidence ? ["", "# Where to look", ev.evidence] : [],
2146
+ "",
2147
+ `# Page content (${partLabel(part.index, part.total)})`,
2148
+ "",
2149
+ chunk
2150
+ ].join("\n");
2151
+ }
2152
+ function renderEvidence(parts, total) {
2153
+ const supports = parts.flatMap((p) => p.supports);
2154
+ const contradicts = parts.flatMap((p) => p.contradicts);
2155
+ const lines = [
2156
+ `The page was too long to read in one pass, so it was read in ${String(total)} parts`,
2157
+ "and the passages bearing on this assertion were collected below. Judge the",
2158
+ "assertion against this evidence. Absence of a quotation is weak evidence of",
2159
+ "absence in the page: parts that bore on nothing contributed nothing.",
2160
+ "",
2161
+ "## Passages supporting the assertion",
2162
+ ...supports.length > 0 ? supports.map((q) => `- ${q}`) : ["(none)"],
2163
+ "",
2164
+ "## Passages contradicting the assertion",
2165
+ ...contradicts.length > 0 ? contradicts.map((q) => `- ${q}`) : ["(none)"]
2166
+ ];
2167
+ return lines.join("\n");
2168
+ }
2169
+
2170
+ // src/docevals/judge/cache.ts
2171
+ var VerdictCache = class _VerdictCache extends JsonCache {
2172
+ /** An ensemble is cacheable only if every run produced a verdict. */
2173
+ static usable(runs) {
2174
+ return runs.every((r) => r.error === void 0 && r.verdict !== void 0);
2175
+ }
2176
+ set(key, value) {
2177
+ if (!_VerdictCache.usable(value)) return;
2178
+ super.set(key, value);
2179
+ }
2180
+ /**
2181
+ * Reading applies the same predicate, which is what makes the property
2182
+ * total rather than merely forward-looking.
2183
+ *
2184
+ * Guarding only the write cannot heal an entry this class did not write —
2185
+ * and those exist: the 65 errored ensembles that prompted ADR 01038 were on
2186
+ * disk before it, and had to be deleted by hand. A committed cache is
2187
+ * exactly the population at risk, because CI replays it with no provider
2188
+ * reachable, so a poisoned entry there is a verdict no future run can
2189
+ * dislodge. Treating it as a miss costs one re-judge and converges;
2190
+ * inheriting `get` unchanged never does.
2191
+ *
2192
+ * The predicate also matches the library's own definition rather than ours:
2193
+ * `computeConsensus` counts a run with no `verdict` as an error vote, so an
2194
+ * entry with neither `verdict` nor `error` — reachable through a truncated
2195
+ * write to these hand-editable JSON files — would otherwise replay as a
2196
+ * permanent human-review with nothing explaining why.
2197
+ */
2198
+ get(key) {
2199
+ const hit = super.get(key);
2200
+ if (hit === void 0) return void 0;
2201
+ return _VerdictCache.usable(hit) ? hit : void 0;
2202
+ }
2203
+ };
2204
+ function judgeCacheBody(chunkChars, text) {
2205
+ return `chunk${String(chunkChars)}
2206
+ ${text}`;
2207
+ }
2208
+ function cacheKey(provider, model, runs, temperature, body, ev) {
2209
+ const evalFingerprint = JSON.stringify({
2210
+ assertion: ev.assertion,
2211
+ evidence: ev.evidence,
2212
+ examples: ev.examples,
2213
+ type: ev.type
2214
+ });
2215
+ return buildCacheKey([
2216
+ provider,
2217
+ model,
2218
+ `v${PROMPT_VERSION}`,
2219
+ `r${runs}`,
2220
+ `t${temperature}`,
2221
+ // Pre-hashed: page bodies are large and key parts should stay short.
2222
+ sha256(body),
2223
+ sha256(evalFingerprint)
2224
+ ]);
2225
+ }
2226
+
2227
+ // src/docevals/judge/self-preference.ts
2228
+ var remedy = (evalId) => `, and it is also the judge. Self-judging favors the author; give "${evalId}" a model: of its own.`;
2229
+ function selfPreferenceOf(plan, ev, model) {
2230
+ const data = plan.page.frontmatter.data;
2231
+ const meta = metaProvenanceEntries(data["meta-provenance"]).filter(
2232
+ (e) => e["generated-by"] === model
2233
+ );
2234
+ const target = ev.target ?? DEFAULT_TARGET;
2235
+ const readsBody = target === "body" || target === "raw";
2236
+ const readsFields = target === "frontmatter" || target === "raw";
2237
+ if (readsBody && provenanceEntries(data.provenance).some((e) => e["generated-by"] === model)) {
2238
+ return {
2239
+ axis: "content",
2240
+ model,
2241
+ message: `provenance names ${model} for the body this eval grades${remedy(ev.name)}`
2242
+ };
2243
+ }
2244
+ if (readsFields && meta.some((e) => e.fields.length > 0)) {
2245
+ return {
2246
+ axis: "content",
2247
+ model,
2248
+ message: `meta-provenance names ${model} for the fields this eval grades${remedy(ev.name)}`
2249
+ };
2250
+ }
2251
+ if (meta.some((e) => e.evals.includes(ev.name))) {
2252
+ return {
2253
+ axis: "criterion",
2254
+ model,
2255
+ message: `meta-provenance says ${model} proposed "${ev.name}"${remedy(ev.name)}`
2256
+ };
2257
+ }
2258
+ return void 0;
2259
+ }
2260
+
2261
+ // src/docevals/core/engine.ts
2262
+ import { resolve as resolve8 } from "node:path";
2263
+ var GENERATION_UNAVAILABLE = "no command and script generation unavailable (configure a provider or run `manni docevals generate`)";
2264
+ function unsupportedTarget(ev) {
2265
+ const target = ev.target;
2266
+ if (target === void 0 || target === "body") return void 0;
2267
+ const grader = graderFor(ev.grader);
2268
+ const named = typeof target === "string" ? target : target.path;
2269
+ if (grader?.targets?.includes(typeof target === "string" ? target : "file")) {
2270
+ return void 0;
2271
+ }
2272
+ return `grader ${ev.grader} cannot read target "${named}" \u2014 it grades the page body. Use the ai grader for this target, or drop the target.`;
2273
+ }
2274
+ function skippedResult(plan, ev, reason) {
2275
+ return {
2276
+ evalName: ev.name,
2277
+ type: ev.type,
2278
+ grader: ev.grader,
2279
+ file: plan.page.file,
2280
+ outcome: "skipped",
2281
+ skipReason: reason,
2282
+ durationMs: 0
2283
+ };
2284
+ }
2285
+ function resultKey(file, evalName) {
2286
+ return JSON.stringify([file, evalName]);
2287
+ }
2288
+ function groupFindings(findings) {
2289
+ const map = /* @__PURE__ */ new Map();
2290
+ for (const f of findings) {
2291
+ const key = resultKey(f.file, f.evalName);
2292
+ const list = map.get(key) ?? [];
2293
+ list.push(f);
2294
+ map.set(key, list);
2295
+ }
2296
+ return map;
2297
+ }
2298
+ function stampSuites(results, plans) {
2299
+ const suiteOf = /* @__PURE__ */ new Map();
2300
+ const weightOf = /* @__PURE__ */ new Map();
2301
+ const locationOf2 = /* @__PURE__ */ new Map();
2302
+ for (const plan of plans) {
2303
+ for (const ev of plan.evals) {
2304
+ const key = resultKey(plan.page.file, ev.name);
2305
+ suiteOf.set(key, ev.suite);
2306
+ weightOf.set(key, ev.weight);
2307
+ locationOf2.set(key, ev.location);
2308
+ }
2309
+ }
2310
+ for (const r of results) {
2311
+ const key = resultKey(r.file, r.evalName);
2312
+ r.suite = suiteOf.get(key) ?? "default";
2313
+ r.weight = weightOf.get(key) ?? 1;
2314
+ r.location = locationOf2.get(key) ?? { file: r.file };
2315
+ }
2316
+ }
2317
+ function resolveBaseline(results, config, options, cwd, problems) {
2318
+ const wants = options.writeBaseline !== void 0 && options.writeBaseline !== false;
2319
+ if (options.baseline === false && !wants) return { results };
2320
+ const readFallback = config.baseline ?? (options.baseline === true ? DEFAULT_BASELINE_PATH : null);
2321
+ const applyFrom = options.baseline === false ? null : typeof options.baseline === "string" ? options.baseline : readFallback;
2322
+ const writeTo = !wants ? null : typeof options.writeBaseline === "string" ? options.writeBaseline : config.baseline ?? DEFAULT_BASELINE_PATH;
2323
+ const configured = writeTo ?? applyFrom;
2324
+ if (configured === null) return { results };
2325
+ const ctx = { base: config.configDir, runBase: cwd };
2326
+ const read = (from) => {
2327
+ try {
2328
+ return readBaseline(resolve8(config.configDir, from), from);
2329
+ } catch (e) {
2330
+ if (!wants) throw e;
2331
+ problems.push({
2332
+ file: from,
2333
+ message: `${errorMessage(e)} \u2014 re-recording over it.`,
2334
+ level: "warning"
2335
+ });
2336
+ return null;
2337
+ }
2338
+ };
2339
+ const previous = applyFrom === null ? null : read(applyFrom);
2340
+ const overwriting = writeTo === null ? null : writeTo === applyFrom ? previous : read(writeTo);
2341
+ let out = results;
2342
+ let recorded = 0;
2343
+ let suppressed = 0;
2344
+ let stale = 0;
2345
+ if (previous && options.baseline !== false) {
2346
+ const applied = applyBaseline(results, previous, ctx);
2347
+ out = applied.results;
2348
+ recorded = applied.recorded;
2349
+ suppressed = applied.suppressed;
2350
+ stale = applied.stale;
2351
+ }
2352
+ if (!wants || writeTo === null) {
2353
+ return {
2354
+ results: out,
2355
+ summary: { path: configured, recorded, suppressed, stale }
2356
+ };
2357
+ }
2358
+ const next = buildBaseline(results, options.toolVersion ?? "unknown", ctx);
2359
+ writeBaselineFile(resolve8(config.configDir, writeTo), next, writeTo);
2360
+ const { added, removed } = diffBaselines(overwriting, next);
2361
+ const accepted = applyBaseline(results, next, ctx);
2362
+ if (config.baseline !== writeTo) {
2363
+ problems.push({
2364
+ file: writeTo,
2365
+ message: `Recorded ${writeTo}, but \`baseline:\` in ${config.configPath} is ${config.baseline === null ? "not set" : `"${config.baseline}"`}. An ordinary run will not read this file \u2014 point the key at it, or pass --baseline ${writeTo}.`,
2366
+ level: "warning"
2367
+ });
2368
+ }
2369
+ return {
2370
+ results: accepted.results,
2371
+ summary: {
2372
+ path: configured,
2373
+ recorded,
2374
+ suppressed,
2375
+ stale,
2376
+ written: { added, removed, total: countFingerprints(next) }
2377
+ }
2378
+ };
2379
+ }
2380
+ function applySelection(plans, config, options, requireRunnable = true) {
2381
+ const names = options.evalNames?.filter((n) => n.trim() !== "") ?? [];
2382
+ const suite = options.suite;
2383
+ if (names.length === 0 && suite === void 0) return false;
2384
+ if (suite !== void 0 && !(suite in config.suites)) {
2385
+ throw new DocevalsError(
2386
+ `--suite "${suite}" is not a defined suite. Defined: ${Object.keys(config.suites).sort().join(", ") || "(none)"}`
2387
+ );
2388
+ }
2389
+ const wanted = new Set(names);
2390
+ let matched = 0;
2391
+ for (const plan of plans) {
2392
+ const runnable = !plan.skip && !plan.problems.some((p) => p.level === "error");
2393
+ plan.evals = plan.evals.filter((ev) => {
2394
+ const byName = wanted.size === 0 || wanted.has(ev.name);
2395
+ const bySuite = suite === void 0 || ev.suite === suite;
2396
+ const keep = byName && bySuite;
2397
+ if (keep && (!requireRunnable || runnable && !ev.skip)) matched += 1;
2398
+ return keep;
2399
+ });
2400
+ }
2401
+ if (matched === 0) {
2402
+ const asked = [
2403
+ names.length > 0 ? `--eval ${names.join(", ")}` : "",
2404
+ suite === void 0 ? "" : `--suite ${suite}`
2405
+ ].filter(Boolean).join(" and ");
2406
+ throw new DocevalsError(
2407
+ `${asked} matched no evals that would run on the pages selected. Nothing would have been checked \u2014 the name may be wrong, or every page carrying it may be skipped. Run \`manni docevals list\` with the same filter to see the resolved plan.`
2408
+ );
2409
+ }
2410
+ return true;
2411
+ }
2412
+ function scopeManifests(plan) {
2413
+ return plan.page.external?.evalManifests ?? [];
2414
+ }
2415
+ function sinceSelects(plan, changed) {
2416
+ return [plan.page.absPath, ...scopeManifests(plan)].some(
2417
+ (f) => changed.has(changedKey(f))
2418
+ );
2419
+ }
2420
+ function applyScope(plans, keep) {
2421
+ let pagesSelected = 0;
2422
+ for (const plan of plans) {
2423
+ if (keep(plan)) {
2424
+ pagesSelected += 1;
2425
+ continue;
2426
+ }
2427
+ plan.evals = [];
2428
+ }
2429
+ return { pagesSelected };
2430
+ }
2431
+ async function recentPages(plans, windowMs, cwd, exec) {
2432
+ const floor = Date.now() - windowMs;
2433
+ const ages = await pageAges(
2434
+ plans.map((p) => ({ page: p.page.absPath, manifests: scopeManifests(p) })),
2435
+ cwd,
2436
+ exec
2437
+ );
2438
+ return new Set(plans.filter((_, i) => (ages[i] ?? Number.NEGATIVE_INFINITY) >= floor));
2439
+ }
2440
+ function summarizeSuites(results, config, partial = false) {
2441
+ const bySuite = /* @__PURE__ */ new Map();
2442
+ for (const r of results) {
2443
+ const suite = r.suite ?? "default";
2444
+ const list = bySuite.get(suite) ?? [];
2445
+ list.push(r);
2446
+ bySuite.set(suite, list);
2447
+ }
2448
+ const summaries = [];
2449
+ for (const [suite, rs] of [...bySuite.entries()].sort()) {
2450
+ const passed = rs.filter((r) => r.outcome === "pass").length;
2451
+ const failed = rs.filter((r) => r.outcome === "fail").length;
2452
+ const errored = rs.filter((r) => r.outcome === "error").length;
2453
+ const needsReview = rs.filter((r) => r.outcome === "needs-review").length;
2454
+ const skipped = rs.filter((r) => r.outcome === "skipped").length;
2455
+ const graded = (r) => r.outcome === "pass" || r.outcome === "fail" || r.outcome === "error";
2456
+ const suiteCriteria = config.suites[suite]?.criteria ?? [];
2457
+ const contributions = [];
2458
+ let critPassed = 0;
2459
+ let critFailed = 0;
2460
+ let critSuspended = 0;
2461
+ const absorbed = /* @__PURE__ */ new Set();
2462
+ const byKey = /* @__PURE__ */ new Map();
2463
+ const filesByEval = /* @__PURE__ */ new Map();
2464
+ for (const r of rs) {
2465
+ byKey.set(resultKey(r.file, r.evalName), r);
2466
+ const files = filesByEval.get(r.evalName) ?? /* @__PURE__ */ new Set();
2467
+ files.add(r.file);
2468
+ filesByEval.set(r.evalName, files);
2469
+ }
2470
+ for (const critName of suiteCriteria) {
2471
+ const def = config.criteria[critName];
2472
+ if (!def) continue;
2473
+ const pages = /* @__PURE__ */ new Set();
2474
+ for (const name of def.evals) {
2475
+ for (const file of filesByEval.get(name) ?? []) pages.add(file);
2476
+ }
2477
+ for (const file of pages) {
2478
+ const members = def.evals.map(
2479
+ (name) => byKey.get(resultKey(file, name))
2480
+ );
2481
+ if (members.some((m) => m === void 0 || !graded(m))) {
2482
+ critSuspended++;
2483
+ continue;
2484
+ }
2485
+ const passes = members.map((m) => m?.outcome === "pass");
2486
+ const passed2 = def.combine === "any" ? passes.some(Boolean) : passes.every(Boolean);
2487
+ if (passed2) critPassed++;
2488
+ else critFailed++;
2489
+ contributions.push({ weight: def.weight, passed: passed2 });
2490
+ for (const m of members) {
2491
+ if (m) absorbed.add(resultKey(m.file, m.evalName));
2492
+ }
2493
+ }
2494
+ }
2495
+ for (const r of rs) {
2496
+ if (!graded(r) || absorbed.has(resultKey(r.file, r.evalName))) continue;
2497
+ contributions.push({ weight: r.weight ?? 1, passed: r.outcome === "pass" });
2498
+ }
2499
+ const totalWeight = contributions.reduce((sum, c) => sum + c.weight, 0);
2500
+ const passRate = totalWeight > 0 ? contributions.filter((c) => c.passed).reduce((sum, c) => sum + c.weight, 0) / totalWeight : 1;
2501
+ const targetPassRate = config.suites[suite]?.targetPassRate ?? 1;
2502
+ summaries.push({
2503
+ suite,
2504
+ total: rs.length,
2505
+ passed,
2506
+ failed,
2507
+ needsReview,
2508
+ skipped,
2509
+ errored,
2510
+ passRate,
2511
+ targetPassRate,
2512
+ // A suite target is a claim about a body of checks. A filtered run
2513
+ // measured part of it, so it reports the numbers and withholds the
2514
+ // verdict — erring toward false confidence is what gets a gate removed
2515
+ // rather than fixed (ADR 01018).
2516
+ meetsTarget: partial ? false : passRate >= targetPassRate,
2517
+ ...partial ? { partial: true } : {},
2518
+ ...suiteCriteria.length > 0 ? {
2519
+ criteria: {
2520
+ total: critPassed + critFailed + critSuspended,
2521
+ passed: critPassed,
2522
+ failed: critFailed,
2523
+ suspended: critSuspended
2524
+ }
2525
+ } : {}
2526
+ });
2527
+ }
2528
+ return summaries;
2529
+ }
2530
+ async function runEvals(options = {}) {
2531
+ const cwd = options.cwd ?? process.cwd();
2532
+ const paths = options.paths ?? [];
2533
+ const config = options.config ?? loadRunConfig(
2534
+ {
2535
+ paths,
2536
+ ...options.configPath === void 0 ? {} : { configPath: options.configPath },
2537
+ ...options.noConfig === void 0 ? {} : { noConfig: options.noConfig },
2538
+ ...options.collection === void 0 ? {} : { collection: options.collection }
2539
+ },
2540
+ cwd
2541
+ );
2542
+ const exec = options.exec ?? realExec;
2543
+ const pages = await withExternalMetadata(
2544
+ discoverPages(
2545
+ config,
2546
+ {
2547
+ paths,
2548
+ verb: "evaluate",
2549
+ ...options.collection === void 0 ? {} : { collection: options.collection },
2550
+ ...options.exclude === void 0 ? {} : { exclude: options.exclude }
2551
+ },
2552
+ cwd
2553
+ ),
2554
+ config,
2555
+ cwd
2556
+ );
2557
+ if (options.writeBaseline !== void 0 && options.writeBaseline !== false && ((options.evalNames?.some((n) => n.trim() !== "") ?? false) || options.suite !== void 0 || options.since !== void 0 || options.newerThan !== void 0)) {
2558
+ throw new DocevalsError(
2559
+ "--write-baseline records the whole corpus, so it cannot be combined with --eval, --suite, --since or --newer-than: the re-record would drop every finding the narrowing excluded. Re-run without it to record."
2560
+ );
2561
+ }
2562
+ const sinceRef = options.since;
2563
+ const newerThan = options.newerThan;
2564
+ const windowMs = newerThan === void 0 ? void 0 : parseDuration(newerThan, (message) => new DocevalsError(message));
2565
+ const changed = sinceRef === void 0 ? null : await changedFilesSince(sinceRef, cwd, exec);
2566
+ const plans = resolvePages(pages, config);
2567
+ assertRegisteredGraders(plans, config);
2568
+ const unskipped = plans.filter((p) => !p.skip);
2569
+ if (unskipped.length > 0 && unskipped.every((p) => p.evals.length === 0) && !plans.some((p) => p.problems.some((pr) => pr.level === "error"))) {
2570
+ throw new DocevalsError(
2571
+ `No evals resolved for any of the ${unskipped.length} page(s) this run would check. Nothing would have been checked, so this run would have reported success without checking anything.
2572
+ Point \`defaults.suite\` at a suite in ${config.configPath}, or give the pages an \`eval-suite\` or \`evals\` frontmatter key. Run \`manni docevals list\` to see the resolved plan.`,
2573
+ "nothing-resolved"
2574
+ );
2575
+ }
2576
+ const filtered = applySelection(plans, config, options);
2577
+ let scope = null;
2578
+ const judgeOptions = options.judgeOptions ?? {};
2579
+ const problems = plans.flatMap(
2580
+ (p) => p.problems.map((pr) => ({
2581
+ // A value a manifest supplied is reported against that manifest, at its
2582
+ // line there: the page has nothing at that pointer to fix.
2583
+ file: pr.file ?? p.page.file,
2584
+ message: pr.message,
2585
+ level: pr.level,
2586
+ line: pr.line
2587
+ }))
2588
+ );
2589
+ const results = [];
2590
+ const deterministicTargets = [];
2591
+ const aiTargets = [];
2592
+ const generationTargets = [];
2593
+ const generatedPaths = [];
2594
+ const generatedThisRun = /* @__PURE__ */ new Set();
2595
+ const granted = grantsFor(config.execution.allow, {
2596
+ ...options.allowExecution === void 0 ? {} : { allowExecution: options.allowExecution },
2597
+ ...options.execution === void 0 ? {} : { execution: options.execution }
2598
+ });
2599
+ const allowFrontmatterCommands = granted.has("frontmatter-commands");
2600
+ problems.push(
2601
+ ...checkFeasibility(plans, {
2602
+ allowFrontmatterCommands,
2603
+ canGenerate: options.generate !== false && options.generateScripts != null,
2604
+ ...options.deterministicOnly === true ? { deterministicOnly: true } : {},
2605
+ ...options.aiOnly === true ? { aiOnly: true } : {}
2606
+ })
2607
+ );
2608
+ if (changed !== null || windowMs !== void 0) {
2609
+ const recent = windowMs === void 0 ? null : await recentPages(plans, windowMs, cwd, exec);
2610
+ scope = {
2611
+ pagesTotal: plans.length,
2612
+ ...applyScope(
2613
+ plans,
2614
+ (plan) => (changed === null || sinceSelects(plan, changed)) && (recent === null || recent.has(plan))
2615
+ )
2616
+ };
2617
+ }
2618
+ for (const plan of plans) {
2619
+ if (plan.problems.some((p) => p.level === "error")) continue;
2620
+ for (const ev of plan.evals) {
2621
+ if (plan.skip) {
2622
+ results.push(skippedResult(plan, ev, "page skipped (evals.skip)"));
2623
+ continue;
2624
+ }
2625
+ if (ev.skip) {
2626
+ results.push(skippedResult(plan, ev, "eval skipped"));
2627
+ continue;
2628
+ }
2629
+ if (ev.grader === "human") {
2630
+ results.push({
2631
+ evalName: ev.name,
2632
+ type: ev.type,
2633
+ grader: ev.grader,
2634
+ file: plan.page.file,
2635
+ outcome: "needs-review",
2636
+ durationMs: 0
2637
+ });
2638
+ continue;
2639
+ }
2640
+ if (ev.grader === "ai") {
2641
+ if (options.deterministicOnly) {
2642
+ results.push(skippedResult(plan, ev, "judge skipped (--deterministic-only)"));
2643
+ } else if (!options.judge) {
2644
+ results.push(skippedResult(plan, ev, "judge unavailable (no provider)"));
2645
+ } else {
2646
+ aiTargets.push({ plan, eval: ev });
2647
+ }
2648
+ continue;
2649
+ }
2650
+ if (options.aiOnly) {
2651
+ results.push(skippedResult(plan, ev, "deterministic evals skipped (--ai-only)"));
2652
+ continue;
2653
+ }
2654
+ const pageAuthoredArgv = ev.source === "page" && (ev.grader === "command" || Array.isArray(ev.options.command));
2655
+ if (pageAuthoredArgv && !allowFrontmatterCommands) {
2656
+ results.push(
2657
+ skippedResult(
2658
+ plan,
2659
+ ev,
2660
+ NOT_GRANTED_REASON
2661
+ )
2662
+ );
2663
+ continue;
2664
+ }
2665
+ if (ev.grader === "command" && !ev.command) {
2666
+ generationTargets.push({ plan, eval: ev });
2667
+ continue;
2668
+ }
2669
+ if (ev.grader === "command" && ev.command && ev.generatedAssertionHash && ev.assertion && ev.generatedAssertionHash !== sha256(ev.assertion)) {
2670
+ if (options.generate !== false && options.generateScripts) {
2671
+ generationTargets.push({ plan, eval: ev });
2672
+ continue;
2673
+ }
2674
+ problems.push({
2675
+ file: plan.page.file,
2676
+ message: `Eval "${ev.name}": assertion changed since its script was generated \u2014 run \`manni docevals generate\` to regenerate`,
2677
+ level: "warning"
2678
+ });
2679
+ }
2680
+ const unsupported = unsupportedTarget(ev);
2681
+ if (unsupported !== void 0) {
2682
+ results.push({
2683
+ evalName: ev.name,
2684
+ type: ev.type,
2685
+ grader: ev.grader,
2686
+ file: plan.page.file,
2687
+ outcome: "error",
2688
+ skipReason: unsupported,
2689
+ durationMs: 0
2690
+ });
2691
+ continue;
2692
+ }
2693
+ deterministicTargets.push({ plan, eval: ev });
2694
+ }
2695
+ }
2696
+ if (generationTargets.length > 0) {
2697
+ if (options.generate !== false && options.generateScripts) {
2698
+ const gen = await options.generateScripts(
2699
+ generationTargets,
2700
+ config,
2701
+ judgeOptions
2702
+ );
2703
+ generatedPaths.push(...gen.generatedPaths);
2704
+ const refusedBy = new Map(
2705
+ (gen.refusals ?? []).map((r) => [resultKey(r.file, r.evalName), r.message])
2706
+ );
2707
+ for (const t of generationTargets) {
2708
+ const refused = refusedBy.get(resultKey(t.plan.page.file, t.eval.name));
2709
+ if (!t.eval.command && refused !== void 0) {
2710
+ results.push({
2711
+ evalName: t.eval.name,
2712
+ type: t.eval.type,
2713
+ grader: t.eval.grader,
2714
+ file: t.plan.page.file,
2715
+ outcome: "error",
2716
+ skipReason: refused,
2717
+ durationMs: 0
2718
+ });
2719
+ } else if (!t.eval.command && gen.unavailable !== void 0) {
2720
+ results.push({
2721
+ evalName: t.eval.name,
2722
+ type: t.eval.type,
2723
+ grader: t.eval.grader,
2724
+ file: t.plan.page.file,
2725
+ outcome: "error",
2726
+ skipReason: `${GENERATION_UNAVAILABLE}: ${gen.unavailable}`,
2727
+ durationMs: 0
2728
+ });
2729
+ } else if (t.eval.command) {
2730
+ deterministicTargets.push(t);
2731
+ generatedThisRun.add(resultKey(t.plan.page.file, t.eval.name));
2732
+ } else {
2733
+ results.push({
2734
+ evalName: t.eval.name,
2735
+ type: t.eval.type,
2736
+ grader: t.eval.grader,
2737
+ file: t.plan.page.file,
2738
+ outcome: "error",
2739
+ skipReason: "script generation failed",
2740
+ generated: false,
2741
+ durationMs: 0
2742
+ });
2743
+ }
2744
+ }
2745
+ } else {
2746
+ for (const t of generationTargets) {
2747
+ results.push({
2748
+ evalName: t.eval.name,
2749
+ type: t.eval.type,
2750
+ grader: t.eval.grader,
2751
+ file: t.plan.page.file,
2752
+ outcome: "error",
2753
+ skipReason: GENERATION_UNAVAILABLE,
2754
+ durationMs: 0
2755
+ });
2756
+ }
2757
+ }
2758
+ }
2759
+ const byKind = /* @__PURE__ */ new Map();
2760
+ for (const t of deterministicTargets) {
2761
+ const list = byKind.get(t.eval.grader) ?? [];
2762
+ list.push(t);
2763
+ byKind.set(t.eval.grader, list);
2764
+ }
2765
+ for (const [kind, targets] of byKind) {
2766
+ const grader = graderFor(kind);
2767
+ if (!grader) {
2768
+ for (const t of targets) {
2769
+ results.push({
2770
+ evalName: t.eval.name,
2771
+ type: t.eval.type,
2772
+ grader: t.eval.grader,
2773
+ file: t.plan.page.file,
2774
+ outcome: "error",
2775
+ skipReason: `Unknown grader kind "${kind}"`,
2776
+ durationMs: 0
2777
+ });
2778
+ }
2779
+ continue;
2780
+ }
2781
+ for (const group of groupTargetsByEval(targets)) {
2782
+ const start = Date.now();
2783
+ let findings;
2784
+ try {
2785
+ findings = await grader.grade({ targets: group, config, root: cwd, exec });
2786
+ } catch (e) {
2787
+ const reason = errorMessage(e);
2788
+ for (const target of group) {
2789
+ results.push({
2790
+ evalName: target.eval.name,
2791
+ suite: target.eval.suite,
2792
+ type: target.eval.type,
2793
+ grader: target.eval.grader,
2794
+ file: target.plan.page.file,
2795
+ outcome: "error",
2796
+ // Names the eval, not just the kind. With one catch per kind the
2797
+ // message described whichever eval happened to throw while sitting
2798
+ // on every eval's result, so the name in the result and the name
2799
+ // in the message disagreed.
2800
+ skipReason: `grader ${kind} failed for eval "${target.eval.name}": ${reason}`,
2801
+ durationMs: Date.now() - start
2802
+ });
2803
+ }
2804
+ continue;
2805
+ }
2806
+ const durationMs = Date.now() - start;
2807
+ const grouped = groupFindings(findings);
2808
+ for (const t of group) {
2809
+ const key = resultKey(t.plan.page.file, t.eval.name);
2810
+ const own = grouped.get(key) ?? [];
2811
+ const hasError = own.some(
2812
+ (f) => f.severity === "error" || f.diagnostic === true
2813
+ );
2814
+ results.push({
2815
+ evalName: t.eval.name,
2816
+ type: t.eval.type,
2817
+ grader: t.eval.grader,
2818
+ file: t.plan.page.file,
2819
+ outcome: hasError ? "fail" : "pass",
2820
+ findings: own.length > 0 ? own : void 0,
2821
+ generated: generatedThisRun.has(key) ? true : void 0,
2822
+ durationMs: Math.round(durationMs / group.length)
2823
+ });
2824
+ }
2825
+ }
2826
+ }
2827
+ let effectiveAiTargets = aiTargets;
2828
+ if (config.defaults.failFast) {
2829
+ const failedPages = new Set(
2830
+ results.filter((r) => r.outcome === "fail" || r.outcome === "error").map((r) => r.file)
2831
+ );
2832
+ effectiveAiTargets = [];
2833
+ for (const t of aiTargets) {
2834
+ if (failedPages.has(t.plan.page.file)) {
2835
+ results.push(
2836
+ skippedResult(t.plan, t.eval, "deterministic-precondition-failed")
2837
+ );
2838
+ } else {
2839
+ effectiveAiTargets.push(t);
2840
+ }
2841
+ }
2842
+ }
2843
+ if (effectiveAiTargets.length > 0 && options.judge) {
2844
+ results.push(
2845
+ ...await options.judge(effectiveAiTargets, config, judgeOptions)
2846
+ );
2847
+ }
2848
+ const budgetSkipped = results.filter((r) => isTurnBudgetSkip(r.skipReason));
2849
+ const firstBudgetSkipped = budgetSkipped[0];
2850
+ if (firstBudgetSkipped !== void 0) {
2851
+ problems.push({
2852
+ file: firstBudgetSkipped.file,
2853
+ message: `${budgetSkipped.length} eval(s) were not judged: the turn budget ran out. This run covered less than it was asked to \u2014 raise --max-turns or narrow the run.`,
2854
+ level: "warning"
2855
+ });
2856
+ }
2857
+ if (!results.some((r) => r.outcome !== "skipped")) {
2858
+ problems.push({
2859
+ file: config.configPath,
2860
+ message: `This run graded nothing \u2014 no eval reached a verdict, so it established nothing about the corpus. ` + (results.length > 0 ? `All ${results.length} resolved eval(s) were skipped; check the skip reasons above \u2014 a page-level \`eval-skip\`, a grader-class flag, or a missing provider.` : (
2861
+ // With zero results the cause is not always the same, and naming the
2862
+ // wrong one sends the reader to the wrong file. An error-level
2863
+ // resolution problem makes the plan loop skip that page entirely and
2864
+ // also suppresses the empty-plan throw above, so "skipped or scoped
2865
+ // out" would contradict the errors printed right beside it.
2866
+ problems.some((p) => p.level === "error") ? `Every discovered page was dropped by an error-level resolution problem \u2014 fix those first; they are listed above.` : `Every discovered page was skipped or scoped out of the run.`
2867
+ )),
2868
+ level: "warning"
2869
+ });
2870
+ }
2871
+ const baselineOutcome = resolveBaseline(results, config, options, cwd, problems);
2872
+ const baselinedResults = [...baselineOutcome.results];
2873
+ results.length = 0;
2874
+ results.push(...baselinedResults);
2875
+ stampSuites(results, plans);
2876
+ for (const r of results) {
2877
+ if (!r.selfPreference) continue;
2878
+ const plan = plans.find((p) => p.page.file === r.file);
2879
+ const ev = plan?.evals.find((e) => e.name === r.evalName);
2880
+ const finding = plan && ev ? selfPreferenceOf(plan, ev, r.selfPreference.model) : void 0;
2881
+ if (!finding) continue;
2882
+ problems.push({ file: r.file, level: "warning", message: finding.message });
2883
+ }
2884
+ const scopeLostCoverage = scope !== null && scope.pagesSelected < plans.length;
2885
+ const suites = summarizeSuites(results, config, filtered || scopeLostCoverage);
2886
+ const judged = results.filter((r) => r.consensus != null);
2887
+ const totalTokens = judged.reduce(
2888
+ (n, r) => n + (r.consensus?.runs.reduce(
2889
+ (m, run) => m + (run.usage ? run.usage.inputTokens + run.usage.outputTokens : 0),
2890
+ 0
2891
+ ) ?? 0),
2892
+ 0
2893
+ );
2894
+ const hasFailure = results.some((r) => r.outcome === "fail" || r.outcome === "error") || suites.some((s) => s.partial !== true && !s.meetsTarget) || problems.some((p) => p.level === "error") || options.failOnReview === true && results.some((r) => r.outcome === "needs-review");
2895
+ return {
2896
+ pages: plans.length,
2897
+ evalResults: results,
2898
+ suites,
2899
+ usage: {
2900
+ totalTokens,
2901
+ cachedEvals: judged.filter(
2902
+ (r) => r.consensus?.runs.every((run) => run.cached) === true
2903
+ ).length,
2904
+ judgedEvals: judged.length
2905
+ },
2906
+ generated: generatedPaths,
2907
+ exitCode: hasFailure ? 1 : 0,
2908
+ problems,
2909
+ ...baselineOutcome.summary ? { baseline: baselineOutcome.summary } : {},
2910
+ ...scope && sinceRef !== void 0 ? { since: { ref: sinceRef, ...scope } } : {},
2911
+ ...scope && newerThan !== void 0 ? { newerThan: { duration: newerThan, ...scope } } : {}
2912
+ };
2913
+ }
2914
+
2915
+ export {
2916
+ splitBody,
2917
+ looksLikeOverflow,
2918
+ partLabel,
2919
+ DEFAULT_CONFIG_FILENAME,
2920
+ parseConfig,
2921
+ loadConfig,
2922
+ loadRunConfig,
2923
+ leadingFrontmatterFormat,
2924
+ stripFrontmatterBlock,
2925
+ readPage,
2926
+ runConfigOptions,
2927
+ documentSet,
2928
+ discoverPages,
2929
+ EVAL_KEYS,
2930
+ EXTERNAL_KEYS,
2931
+ urlManifestRefusal,
2932
+ loadExternalReader,
2933
+ withExternalMetadata,
2934
+ frontmatterSchema,
2935
+ FRONTMATTER_SCHEMA_ID,
2936
+ readTarget,
2937
+ graderFor,
2938
+ listGraderKinds,
2939
+ registerGrader,
2940
+ resolvePage,
2941
+ resolvePages,
2942
+ assertRegisteredGraders,
2943
+ JUDGE_SYSTEM_PROMPT,
2944
+ buildUserContent,
2945
+ EVIDENCE_SYSTEM_PROMPT,
2946
+ EVIDENCE_SCHEMA,
2947
+ buildEvidenceUser,
2948
+ renderEvidence,
2949
+ sha256,
2950
+ VerdictCache,
2951
+ judgeCacheBody,
2952
+ cacheKey,
2953
+ selfPreferenceOf,
2954
+ applySelection,
2955
+ runEvals
2956
+ };
2957
+ //# sourceMappingURL=chunk-NWZ2SLYL.js.map