@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,1287 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ EVALS_KEY,
4
+ EvalWriter,
5
+ META_PROVENANCE_KEY,
6
+ SCRIPTGEN_SYSTEM_PROMPT,
7
+ appendPageEvals,
8
+ assertProviderSelection,
9
+ buildScriptgenUser,
10
+ constructProvider,
11
+ entryObject,
12
+ hasEditableEval,
13
+ loadReviews,
14
+ makeGenerateScripts,
15
+ makeJudge,
16
+ makeProvider,
17
+ manifestEvalList,
18
+ recordReview,
19
+ resolveProviderIdentity,
20
+ scriptLocationFor,
21
+ selectProvider,
22
+ updateConfigEval,
23
+ updateManifestEval,
24
+ updatePageEval
25
+ } from "./chunk-PHF3D7ER.js";
26
+ import {
27
+ DEFAULT_CONFIG_FILENAME,
28
+ assertRegisteredGraders,
29
+ discoverPages,
30
+ documentSet,
31
+ leadingFrontmatterFormat,
32
+ loadConfig,
33
+ loadExternalReader,
34
+ loadRunConfig,
35
+ looksLikeOverflow,
36
+ partLabel,
37
+ readPage,
38
+ resolvePage,
39
+ resolvePages,
40
+ runConfigOptions,
41
+ sha256,
42
+ splitBody,
43
+ withExternalMetadata
44
+ } from "./chunk-NWZ2SLYL.js";
45
+ import {
46
+ isTurnBudgetSkip
47
+ } from "./chunk-LJ2RDH3F.js";
48
+ import {
49
+ SUMMARY_FORMATS,
50
+ parseFormat
51
+ } from "./chunk-ZXRAE55H.js";
52
+ import {
53
+ DocevalsError
54
+ } from "./chunk-SE4JN5XN.js";
55
+ import {
56
+ SEVERITIES
57
+ } from "./chunk-77AINBYM.js";
58
+ import {
59
+ mergeMetaProvenance
60
+ } from "./chunk-K3D6FPV6.js";
61
+ import {
62
+ palette
63
+ } from "./chunk-RNVYGWIC.js";
64
+ import {
65
+ warn
66
+ } from "./chunk-Q35YIDBV.js";
67
+ import {
68
+ errorMessage
69
+ } from "./chunk-G3T47CFV.js";
70
+
71
+ // src/docevals/commands/generate.ts
72
+ async function runGenerate(paths, options = {}) {
73
+ const cwd = options.cwd ?? process.cwd();
74
+ const config = loadRunConfig(runConfigOptions(paths, options), cwd);
75
+ const flags = { provider: options.provider, model: options.model, local: options.local };
76
+ assertProviderSelection(selectProvider(config, flags));
77
+ const pages = await withExternalMetadata(
78
+ discoverPages(config, documentSet(paths, options, "read"), cwd),
79
+ config,
80
+ cwd
81
+ );
82
+ const plans = resolvePages(pages, config);
83
+ assertRegisteredGraders(plans, config);
84
+ const targets = [];
85
+ const seenConfigEvals = /* @__PURE__ */ new Set();
86
+ for (const plan of plans) {
87
+ if (plan.skip || plan.problems.some((p) => p.level === "error")) continue;
88
+ for (const ev of plan.evals) {
89
+ if (ev.skip || ev.grader !== "command") continue;
90
+ const missing = !ev.command;
91
+ const stale = ev.command != null && ev.generatedAssertionHash != null && ev.assertion != null && ev.generatedAssertionHash !== sha256(ev.assertion);
92
+ if ((missing || stale) && ev.assertion) {
93
+ if (ev.source === "config") {
94
+ if (seenConfigEvals.has(ev.name)) continue;
95
+ seenConfigEvals.add(ev.name);
96
+ }
97
+ targets.push({ plan, eval: ev });
98
+ }
99
+ }
100
+ }
101
+ if (targets.length === 0) return { generatedPaths: [], targets: 0, refusals: [] };
102
+ const provider = options.providerInstance ?? await makeProvider(config, flags);
103
+ const generate = makeGenerateScripts({ provider, root: cwd });
104
+ const { generatedPaths, refusals } = await generate(targets, config, {});
105
+ return { generatedPaths, targets: targets.length, refusals: refusals ?? [] };
106
+ }
107
+
108
+ // src/docevals/commands/fill.ts
109
+ import { writeFileSync as writeFileSync2 } from "node:fs";
110
+ import { resolve } from "node:path";
111
+
112
+ // src/docevals/fill/cache.ts
113
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
114
+ import { join } from "node:path";
115
+
116
+ // src/docevals/fill/prompt.ts
117
+ import { Ajv2020 } from "ajv/dist/2020.js";
118
+ var FILL_PROMPT_VERSION = 4;
119
+ var FILL_SYSTEM_PROMPT = [
120
+ "You propose evals for documentation pages. An eval is a plain-language",
121
+ "assertion about a page that an LLM judge will grade against the page",
122
+ "content on every future run \u2014 a durable quality contract, not a one-off",
123
+ "observation.",
124
+ "",
125
+ "Rules for every proposal:",
126
+ "- Assert durable, page-specific properties a maintainer would want",
127
+ " guarded (structure, coverage, accuracy anchors) \u2014 never incidental",
128
+ " phrasing, styling, or facts likely to change by design.",
129
+ "- The assertion must be judgeable from the page content alone.",
130
+ "- Ids are short kebab-case identifiers, unique on the page.",
131
+ "- Provide examples.pass and examples.fail: one sentence each describing",
132
+ " a state of the page that satisfies or violates the assertion.",
133
+ "- Report an honest confidence between 0 and 1 that the assertion is",
134
+ " correct, checkable, and worth guarding. Do not inflate it.",
135
+ "- Do not duplicate or rephrase the page's existing evals.",
136
+ "- Do NOT restate the page. An assertion derived from the page's own",
137
+ " headings, or lifted from a style guide, passes by construction and keeps",
138
+ " passing however the page rots. Anchor on what a reader would notice if it",
139
+ " broke.",
140
+ "- Prefer the cheapest check that can express the assertion. A verifiable",
141
+ " fact \u2014 a string that must appear, a file that must exist \u2014 belongs in a",
142
+ " deterministic eval; propose a judged assertion only for what genuinely",
143
+ " needs reading comprehension.",
144
+ "- A check lifted from a style rule is secondary: propose it at",
145
+ " severity warning, never as a page's only guard.",
146
+ "- Do not soften an assertion until it always passes. An eval that cannot",
147
+ " fail is a vanity metric \u2014 if you cannot state a version that would catch",
148
+ " a real regression, propose nothing for it.",
149
+ "- Propose at most the requested number, fewer when the page offers",
150
+ " little worth guarding, and nothing at all when nothing qualifies."
151
+ ].join("\n");
152
+ var PROPOSAL_SCHEMA = {
153
+ type: "object",
154
+ required: ["evals"],
155
+ properties: {
156
+ evals: {
157
+ type: "array",
158
+ items: {
159
+ type: "object",
160
+ required: ["id", "assertion", "confidence", "examples"],
161
+ properties: {
162
+ id: { type: "string", pattern: "^[a-z0-9][a-z0-9-]*$" },
163
+ assertion: { type: "string", minLength: 1 },
164
+ type: { enum: ["capability", "regression"] },
165
+ evidence: { type: "string" },
166
+ examples: {
167
+ type: "object",
168
+ required: ["pass", "fail"],
169
+ properties: {
170
+ pass: { type: "string" },
171
+ fail: { type: "string" }
172
+ },
173
+ additionalProperties: false
174
+ },
175
+ severity: { enum: SEVERITIES },
176
+ confidence: { type: "number", minimum: 0, maximum: 1 },
177
+ rationale: { type: "string" }
178
+ },
179
+ additionalProperties: false
180
+ }
181
+ }
182
+ },
183
+ additionalProperties: false
184
+ };
185
+ var ajv = new Ajv2020({ allErrors: true });
186
+ var validateProposal = ajv.compile(
187
+ PROPOSAL_SCHEMA
188
+ );
189
+ function isValidProposal(value) {
190
+ return validateProposal(value);
191
+ }
192
+ function buildFillUser(file, body, existing, maxEvals, part) {
193
+ const existingLines = existing.length === 0 ? ["(none)"] : existing.map(
194
+ (e) => e.assertion ? `- ${e.id}: ${e.assertion}` : `- ${e.id}`
195
+ );
196
+ return [
197
+ "# Page path",
198
+ file,
199
+ "",
200
+ "# Existing evals (do not duplicate these)",
201
+ ...existingLines,
202
+ "",
203
+ "# Maximum proposals",
204
+ String(maxEvals),
205
+ "",
206
+ part === void 0 ? "# Page content" : `# Page content (${partLabel(part.index, part.total)})`,
207
+ "",
208
+ // No cap. A page longer than one call is split and proposed against in
209
+ // parts; truncating instead meant proposing evals for a page whose second
210
+ // half the model never saw, with nothing in the output to say so.
211
+ body
212
+ ].join("\n");
213
+ }
214
+
215
+ // src/docevals/fill/cache.ts
216
+ function fillCacheKey(provider, model, temperature, maxEvals, body, existingNames, chunkChars) {
217
+ return sha256(
218
+ [
219
+ provider,
220
+ model,
221
+ `fill-v${FILL_PROMPT_VERSION}`,
222
+ `t${temperature}`,
223
+ `n${maxEvals}`,
224
+ // The chunk budget decides how the page was split, and so which
225
+ // proposals came back. Without it, two runs at different budgets share
226
+ // a key and the second silently replays the first — and halve-and-retry
227
+ // makes that a real collision, not a hypothetical one.
228
+ `chunk${chunkChars}`,
229
+ sha256(body),
230
+ existingNames.join(",")
231
+ ].join("|")
232
+ );
233
+ }
234
+ var FillCache = class {
235
+ constructor(dir, enabled = true) {
236
+ this.dir = dir;
237
+ this.enabled = enabled;
238
+ }
239
+ dir;
240
+ enabled;
241
+ /** Cache-write failures warn once per run, not once per page. */
242
+ warned = false;
243
+ get(key) {
244
+ if (!this.enabled) return void 0;
245
+ const path = join(this.dir, `${key}.json`);
246
+ if (!existsSync(path)) return void 0;
247
+ try {
248
+ const proposal = JSON.parse(readFileSync(path, "utf8"));
249
+ if (!isValidProposal(proposal)) return void 0;
250
+ return proposal;
251
+ } catch {
252
+ return void 0;
253
+ }
254
+ }
255
+ set(key, proposal) {
256
+ if (!this.enabled) return;
257
+ try {
258
+ mkdirSync(this.dir, { recursive: true });
259
+ writeFileSync(
260
+ join(this.dir, `${key}.json`),
261
+ JSON.stringify(proposal, null, 2)
262
+ );
263
+ } catch (e) {
264
+ if (!this.warned) {
265
+ this.warned = true;
266
+ warn(
267
+ `could not write the fill cache at ${this.dir} (${errorMessage(e)}). Continuing without caching.`
268
+ );
269
+ }
270
+ }
271
+ }
272
+ };
273
+
274
+ // src/docevals/commands/fill.ts
275
+ function toEntry(p) {
276
+ return {
277
+ id: p.id,
278
+ assertion: p.assertion,
279
+ type: p.type ?? "regression",
280
+ grader: "ai",
281
+ evidence: p.evidence,
282
+ examples: p.examples,
283
+ severity: p.severity
284
+ };
285
+ }
286
+ async function runFill(paths, options = {}) {
287
+ const cwd = options.cwd ?? process.cwd();
288
+ let config = loadRunConfig(runConfigOptions(paths, options), cwd);
289
+ const flags = { provider: options.provider, model: options.model, local: options.local };
290
+ assertProviderSelection(selectProvider(config, flags));
291
+ const load = async () => resolvePages(
292
+ await withExternalMetadata(
293
+ discoverPages(config, documentSet(paths, options, "fill"), cwd),
294
+ config,
295
+ cwd
296
+ ),
297
+ config
298
+ );
299
+ let plans = await load();
300
+ const writerFor = () => EvalWriter.for(config, cwd, paths);
301
+ let writer = writerFor();
302
+ if (options.confirm !== void 0 && options.dryRun !== true) {
303
+ const flagged = [];
304
+ for (const plan of plans) {
305
+ if (plan.skip || plan.problems.some((p) => p.level === "error")) continue;
306
+ const home = await writer.homeFor(
307
+ plan.page.file,
308
+ plan.page.frontmatter.data,
309
+ EVALS_KEY
310
+ );
311
+ if (home.kind === "page" && home.prefersExternal) {
312
+ flagged.push({ label: plan.page.file, key: EVALS_KEY, home: home.proposed });
313
+ }
314
+ }
315
+ const applied = await writer.offer(flagged, {
316
+ confirm: options.confirm,
317
+ ...options.onNotice === void 0 ? {} : { onNotice: options.onNotice },
318
+ ...options.onRelocated === void 0 ? {} : { onRelocated: options.onRelocated }
319
+ });
320
+ if (applied) {
321
+ config = loadRunConfig(runConfigOptions(paths, options), cwd);
322
+ plans = await load();
323
+ writer = writerFor();
324
+ }
325
+ }
326
+ const threshold = options.confidence ?? config.fill.confidenceThreshold;
327
+ const maxTurns = options.maxTurns ?? config.fill.maxTurns;
328
+ const temperature = config.fill.temperature;
329
+ const maxEvals = config.fill.maxEvalsPerPage;
330
+ const chunkChars = options.chunkChars ?? config.fill.chunkChars;
331
+ const cache = new FillCache(
332
+ resolve(cwd, config.fill.cacheDir),
333
+ !options.noCache
334
+ );
335
+ const injected = options.providerInstance;
336
+ let resolving;
337
+ const resolveOnce = () => resolving ??= resolveProviderIdentity(config, flags);
338
+ const getIdentity = async () => injected ? { provider: injected.provider(), model: injected.modelName() } : resolveOnce();
339
+ let provider = injected;
340
+ const getProvider = async () => provider ??= constructProvider(config, await resolveOnce());
341
+ let turns = 0;
342
+ const results = [];
343
+ for (const plan of plans) {
344
+ results.push(await fillOne(plan));
345
+ }
346
+ for (const line of writer.warnings(options.dryRun === true)) warn(line);
347
+ return {
348
+ results,
349
+ threshold,
350
+ turns,
351
+ exitCode: results.some((r) => r.status === "error") ? 1 : 0
352
+ };
353
+ async function fillOne(plan) {
354
+ const base = {
355
+ file: plan.page.file,
356
+ status: "nothing-proposed",
357
+ written: [],
358
+ belowThreshold: [],
359
+ capped: [],
360
+ duplicates: [],
361
+ cached: false
362
+ };
363
+ if (plan.skip) return { ...base, status: "skipped" };
364
+ const problem = plan.problems.find((p) => p.level === "error");
365
+ if (problem) return { ...base, status: "error", error: problem.message };
366
+ const format = leadingFrontmatterFormat(plan.page.content);
367
+ if (format === "toml" || format === "json") {
368
+ return {
369
+ ...base,
370
+ status: "error",
371
+ error: `only YAML frontmatter can be filled (found ${format} frontmatter)`
372
+ };
373
+ }
374
+ const existing = plan.evals.map((e) => ({
375
+ id: e.name,
376
+ assertion: e.assertion
377
+ }));
378
+ const existingNames = existing.map((e) => e.id).sort();
379
+ const identity = await getIdentity();
380
+ const keyFor = (budget) => fillCacheKey(
381
+ identity.provider,
382
+ identity.model,
383
+ temperature,
384
+ maxEvals,
385
+ plan.page.body,
386
+ existingNames,
387
+ budget
388
+ );
389
+ const key = keyFor(chunkChars);
390
+ let raw = cache.get(key);
391
+ const cached = raw !== void 0;
392
+ if (!raw) {
393
+ if (maxTurns !== null && turns >= maxTurns) {
394
+ return { ...base, status: "skipped-budget" };
395
+ }
396
+ let budget = chunkChars;
397
+ let partsRead = 0;
398
+ let merged2;
399
+ let failure;
400
+ for (let attempt = 0; attempt < 2 && merged2 === void 0; attempt++) {
401
+ const chunks = splitBody(plan.page.body, budget);
402
+ const collected = /* @__PURE__ */ new Map();
403
+ let overflowed = false;
404
+ let outOfTurns = false;
405
+ partsRead = 0;
406
+ for (const [i, chunk] of chunks.entries()) {
407
+ if (maxTurns !== null && turns >= maxTurns) {
408
+ outOfTurns = true;
409
+ break;
410
+ }
411
+ turns += 1;
412
+ partsRead += 1;
413
+ let response;
414
+ try {
415
+ response = await (await getProvider()).completeJSON({
416
+ system: FILL_SYSTEM_PROMPT,
417
+ user: buildFillUser(
418
+ plan.page.file,
419
+ chunk,
420
+ existing,
421
+ maxEvals,
422
+ chunks.length > 1 ? { index: i, total: chunks.length } : void 0
423
+ ),
424
+ schema: PROPOSAL_SCHEMA,
425
+ temperature
426
+ });
427
+ } catch (e) {
428
+ const message = errorMessage(e);
429
+ failure = message;
430
+ if (looksLikeOverflow(message)) overflowed = true;
431
+ break;
432
+ }
433
+ if (!isValidProposal(response.json)) {
434
+ failure = "provider returned a proposal that does not match the schema";
435
+ break;
436
+ }
437
+ for (const proposal of response.json.evals) {
438
+ const held = collected.get(proposal.id);
439
+ if (held === void 0 || proposal.confidence > held.confidence) {
440
+ collected.set(proposal.id, proposal);
441
+ }
442
+ }
443
+ if (i === chunks.length - 1) merged2 = collected;
444
+ }
445
+ if (outOfTurns) {
446
+ return {
447
+ ...base,
448
+ status: "skipped-budget",
449
+ ...chunks.length > 1 ? {
450
+ error: `--max-turns reached after ${String(partsRead)} of ${String(chunks.length)} part(s) of this page`
451
+ } : {}
452
+ };
453
+ }
454
+ if (merged2 === void 0 && overflowed && attempt === 0) {
455
+ budget = Math.max(1, Math.floor(budget / 2));
456
+ continue;
457
+ }
458
+ if (merged2 === void 0) break;
459
+ }
460
+ if (merged2 === void 0) {
461
+ return {
462
+ ...base,
463
+ status: "error",
464
+ error: failure ?? "the provider returned no proposal"
465
+ };
466
+ }
467
+ raw = { evals: [...merged2.values()] };
468
+ cache.set(keyFor(budget), raw);
469
+ }
470
+ const seen = new Set(existingNames);
471
+ const duplicates = [];
472
+ const fresh = [];
473
+ for (const p of raw.evals) {
474
+ if (seen.has(p.id)) {
475
+ duplicates.push(p.id);
476
+ continue;
477
+ }
478
+ seen.add(p.id);
479
+ fresh.push(p);
480
+ }
481
+ const capped = fresh.slice(maxEvals);
482
+ const belowThreshold = [];
483
+ const written = [];
484
+ for (const p of fresh.slice(0, maxEvals)) {
485
+ if (p.confidence >= threshold) written.push(p);
486
+ else belowThreshold.push(p);
487
+ }
488
+ const result = {
489
+ ...base,
490
+ written,
491
+ belowThreshold,
492
+ capped,
493
+ duplicates,
494
+ cached
495
+ };
496
+ if (written.length === 0) return result;
497
+ const label = plan.page.file;
498
+ const data = plan.page.frontmatter.data;
499
+ let evalsHome;
500
+ let metaHome;
501
+ try {
502
+ evalsHome = await writer.homeFor(label, data, EVALS_KEY);
503
+ metaHome = await writer.homeFor(label, data, META_PROVENANCE_KEY);
504
+ } catch (e) {
505
+ return { ...result, status: "error", error: errorMessage(e), written: [] };
506
+ }
507
+ if (evalsHome.kind === "url") throw writer.urlRefusal(evalsHome, EVALS_KEY);
508
+ if (evalsHome.kind === "no-entry") {
509
+ return { ...result, status: "error", error: evalsHome.message, written: [] };
510
+ }
511
+ const merged = metaHome.kind === "url" || metaHome.kind === "no-entry" ? void 0 : mergeMetaProvenance(
512
+ data[META_PROVENANCE_KEY],
513
+ identity.model,
514
+ "evals",
515
+ written.map((p) => ({ name: p.id, confidence: p.confidence }))
516
+ );
517
+ const metaManifest = metaHome.kind === "manifest" ? metaHome : void 0;
518
+ let metaProvenance;
519
+ if (metaHome.kind === "url") {
520
+ metaProvenance = { written: false, skipReason: "manifest-owned", manifest: metaHome.file };
521
+ } else if (metaHome.kind === "no-entry") {
522
+ metaProvenance = { written: false, skipReason: "unwritable" };
523
+ } else if (merged === void 0) {
524
+ metaProvenance = { written: false, skipReason: "schema-mismatch" };
525
+ } else if (metaManifest !== void 0) {
526
+ metaProvenance = {
527
+ written: true,
528
+ entry: { ...merged.entry, evals: merged.names },
529
+ destination: metaManifest.file
530
+ };
531
+ } else {
532
+ metaProvenance = { written: true, entry: { ...merged.entry, evals: merged.names } };
533
+ }
534
+ const manifest = evalsHome.kind === "manifest" ? evalsHome : void 0;
535
+ const pageEntries = manifest === void 0 ? written.map(toEntry) : [];
536
+ const pageMeta = metaProvenance.written && metaManifest === void 0 ? merged?.list : void 0;
537
+ try {
538
+ if (pageEntries.length > 0 || pageMeta !== void 0) {
539
+ const updated = appendPageEvals(
540
+ plan.page.content,
541
+ label,
542
+ pageEntries,
543
+ pageMeta
544
+ );
545
+ if (!options.dryRun && updated !== plan.page.content) {
546
+ writeFileSync2(plan.page.absPath, updated);
547
+ }
548
+ }
549
+ if (!options.dryRun && manifest !== void 0) {
550
+ const held = manifestEvalList(
551
+ await writer.readManifest(manifest, EVALS_KEY),
552
+ manifest.file,
553
+ manifest.entry
554
+ );
555
+ await writer.writeManifest(manifest, EVALS_KEY, [
556
+ ...held,
557
+ ...written.map((p) => entryObject(toEntry(p)))
558
+ ]);
559
+ }
560
+ if (!options.dryRun && metaManifest !== void 0 && merged !== void 0) {
561
+ await writer.writeManifest(metaManifest, META_PROVENANCE_KEY, merged.list);
562
+ }
563
+ } catch (e) {
564
+ return {
565
+ ...result,
566
+ status: "error",
567
+ error: errorMessage(e),
568
+ written: []
569
+ };
570
+ }
571
+ if (evalsHome.kind === "page" && evalsHome.prefersExternal) {
572
+ writer.stayedOnPage(label, EVALS_KEY, evalsHome.proposed);
573
+ }
574
+ return {
575
+ ...result,
576
+ status: options.dryRun ? "proposed" : "filled",
577
+ wroteTo: manifest === void 0 ? "page" : manifest.file,
578
+ metaProvenance
579
+ };
580
+ }
581
+ }
582
+ var STATUS_LABELS = {
583
+ filled: "filled",
584
+ proposed: "proposed",
585
+ "nothing-proposed": "no-op",
586
+ skipped: "skipped",
587
+ "skipped-budget": "skipped",
588
+ error: "error"
589
+ };
590
+ function renderFill(report, format, opts = {}) {
591
+ parseFormat(format, SUMMARY_FORMATS, "format");
592
+ if (format === "json") return JSON.stringify(report, null, 2);
593
+ const pc = palette(opts.color === true);
594
+ const lines = [];
595
+ const names = (evals) => evals.map((p) => `${p.id} ${p.confidence.toFixed(2)}`).join(", ");
596
+ for (const r of report.results) {
597
+ const label = STATUS_LABELS[r.status].padEnd(8);
598
+ const cachedTag = r.cached ? " [cached]" : "";
599
+ switch (r.status) {
600
+ case "filled":
601
+ lines.push(
602
+ `${pc.green(label)} ${r.file} +${r.written.length} evals (${names(r.written)})${cachedTag}`
603
+ );
604
+ break;
605
+ case "proposed":
606
+ lines.push(
607
+ `${pc.cyan(label)} ${r.file} +${r.written.length} evals (${names(r.written)})${cachedTag} \u2014 dry run, not written`
608
+ );
609
+ break;
610
+ case "nothing-proposed":
611
+ lines.push(`${pc.dim(label)} ${r.file} (nothing new proposed)${cachedTag}`);
612
+ break;
613
+ case "skipped":
614
+ lines.push(`${pc.dim(label)} ${r.file} (evals.skip)`);
615
+ break;
616
+ case "skipped-budget":
617
+ lines.push(`${pc.yellow(label)} ${r.file} (turn budget exhausted)`);
618
+ break;
619
+ case "error":
620
+ lines.push(`${pc.red(label)} ${r.file}: ${r.error ?? "unknown error"}`);
621
+ break;
622
+ }
623
+ if (r.wroteTo !== void 0 && r.wroteTo !== "page") {
624
+ lines.push(` ${pc.cyan("evals")} \u2192 ${r.wroteTo}`);
625
+ }
626
+ const record = r.metaProvenance;
627
+ if (record?.written === true) {
628
+ const destination = record.destination === void 0 ? "" : ` \u2192 ${record.destination}`;
629
+ lines.push(
630
+ ` ${pc.cyan("meta-provenance")} ${record.entry["generated-by"]}: ${record.entry.evals.join(", ")}${destination}`
631
+ );
632
+ } else if (record?.skipReason === "schema-mismatch") {
633
+ lines.push(
634
+ pc.dim(" meta-provenance not written: this page's schemas do not allow it")
635
+ );
636
+ } else if (record?.skipReason === "manifest-owned") {
637
+ lines.push(
638
+ pc.dim(
639
+ ` meta-provenance not written: owned by manifest ${record.manifest}, which is fetched and cannot be written`
640
+ )
641
+ );
642
+ } else if (record?.skipReason === "unwritable") {
643
+ lines.push(
644
+ pc.dim(" meta-provenance not written: its manifest has no entry for this page")
645
+ );
646
+ }
647
+ if (r.belowThreshold.length > 0) {
648
+ lines.push(
649
+ pc.dim(
650
+ ` below ${report.threshold}: ${names(r.belowThreshold)}`
651
+ )
652
+ );
653
+ }
654
+ if (r.capped.length > 0) {
655
+ lines.push(
656
+ pc.dim(` over per-page cap: ${names(r.capped)}`)
657
+ );
658
+ }
659
+ if (r.duplicates.length > 0) {
660
+ lines.push(pc.dim(` duplicates: ${r.duplicates.join(", ")}`));
661
+ }
662
+ }
663
+ lines.push("");
664
+ lines.push(
665
+ `Threshold: ${report.threshold} \xB7 inference calls: ${report.turns}`
666
+ );
667
+ return lines.join("\n");
668
+ }
669
+
670
+ // src/docevals/commands/promote.ts
671
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
672
+ import { dirname, relative } from "node:path";
673
+ var PROMOTE_SCHEMA = {
674
+ type: "object",
675
+ required: ["promotable", "rationale"],
676
+ properties: {
677
+ promotable: {
678
+ type: "boolean",
679
+ description: "True only when the assertion can be fully checked by a deterministic script with no semantic judgment."
680
+ },
681
+ rationale: { type: "string" },
682
+ code: {
683
+ type: "string",
684
+ description: "The check script source, required when promotable is true."
685
+ }
686
+ },
687
+ additionalProperties: false
688
+ };
689
+ var PROMOTE_SYSTEM = [
690
+ "You review documentation eval assertions and decide whether each can be",
691
+ "verified by a deterministic script instead of LLM judgment. Promote only",
692
+ "assertions that are fully checkable with string/regex/structural logic \u2014",
693
+ "anything requiring semantic interpretation stays with the LLM judge.",
694
+ "",
695
+ SCRIPTGEN_SYSTEM_PROMPT,
696
+ "",
697
+ 'Respond with JSON: { "promotable": bool, "rationale": "...", "code": "..." }',
698
+ "(omit code when not promotable)."
699
+ ].join("\n");
700
+ async function assess(provider, target) {
701
+ try {
702
+ const response = await provider.completeJSON({
703
+ system: PROMOTE_SYSTEM,
704
+ user: buildScriptgenUser(
705
+ target.eval.assertion ?? "",
706
+ target.eval.name,
707
+ target.plan.page.file,
708
+ target.plan.page.body
709
+ ),
710
+ schema: PROMOTE_SCHEMA,
711
+ temperature: 0
712
+ });
713
+ const json = response.json;
714
+ return {
715
+ promotable: json.promotable === true,
716
+ rationale: typeof json.rationale === "string" ? json.rationale : "",
717
+ code: typeof json.code === "string" ? json.code : void 0
718
+ };
719
+ } catch (e) {
720
+ return {
721
+ promotable: false,
722
+ rationale: `assessment failed: ${errorMessage(e)}`
723
+ };
724
+ }
725
+ }
726
+ async function runPromote(paths, options = {}) {
727
+ const cwd = options.cwd ?? process.cwd();
728
+ const config = loadRunConfig(runConfigOptions(paths, options), cwd);
729
+ const flags = { provider: options.provider, model: options.model, local: options.local };
730
+ assertProviderSelection(selectProvider(config, flags));
731
+ const pages = await withExternalMetadata(
732
+ discoverPages(config, documentSet(paths, options, "read"), cwd),
733
+ config,
734
+ cwd
735
+ );
736
+ const plans = resolvePages(pages, config);
737
+ assertRegisteredGraders(plans, config);
738
+ let provider = options.providerInstance;
739
+ const getProvider = async () => provider ??= await makeProvider(config, flags);
740
+ const proposals = [];
741
+ const seenConfigEvals = /* @__PURE__ */ new Set();
742
+ let writer;
743
+ const getWriter = () => writer ??= EvalWriter.for(config, cwd, paths);
744
+ for (const plan of plans) {
745
+ if (plan.skip || plan.problems.some((p) => p.level === "error")) continue;
746
+ for (const ev of plan.evals) {
747
+ if (ev.skip || ev.grader !== "ai" || !ev.assertion) continue;
748
+ if (ev.source === "config") {
749
+ if (seenConfigEvals.has(ev.name)) continue;
750
+ seenConfigEvals.add(ev.name);
751
+ }
752
+ const target = { plan, eval: ev };
753
+ const assessment = await assess(await getProvider(), target);
754
+ const proposal = {
755
+ file: plan.page.file,
756
+ evalName: ev.name,
757
+ source: ev.source,
758
+ promotable: assessment.promotable,
759
+ rationale: assessment.rationale,
760
+ applied: false
761
+ };
762
+ if (assessment.promotable && assessment.code && options.write) {
763
+ const location = scriptLocationFor(target, config, cwd);
764
+ mkdirSync2(dirname(location.scriptAbsPath), { recursive: true });
765
+ writeFileSync3(location.scriptAbsPath, assessment.code);
766
+ proposal.scriptPath = relative(cwd, location.scriptAbsPath).replace(
767
+ /\\/g,
768
+ "/"
769
+ );
770
+ const updates = {
771
+ grader: "command",
772
+ command: location.command,
773
+ // The kebab key `applyUpdates` actually reads. This was the
774
+ // pre-1.0 `generated: { assertionHash }` wrapper, which the update
775
+ // type does not consume, so promoted evals were written with no
776
+ // hash and never went stale when their assertion changed
777
+ // (renamed by ADR 01009).
778
+ "generated-assertion-hash": sha256(ev.assertion)
779
+ };
780
+ const home = ev.source === "page" ? await getWriter().homeFor(
781
+ plan.page.file,
782
+ plan.page.frontmatter.data,
783
+ EVALS_KEY
784
+ ) : void 0;
785
+ if (home?.kind === "url") {
786
+ throw getWriter().urlRefusal(home, EVALS_KEY);
787
+ }
788
+ if (home?.kind === "no-entry") {
789
+ proposal.error = home.message;
790
+ } else if (home?.kind === "manifest") {
791
+ const at = getWriter();
792
+ const list = manifestEvalList(
793
+ await at.readManifest(home, EVALS_KEY),
794
+ home.file,
795
+ home.entry
796
+ );
797
+ const next = updateManifestEval(list, ev.name, updates);
798
+ if (next === void 0) {
799
+ proposal.error = `${home.file}: eval "${ev.name}" not found in the entry for ${home.entry}`;
800
+ } else {
801
+ await at.writeManifest(home, EVALS_KEY, next);
802
+ proposal.applied = true;
803
+ }
804
+ } else if (ev.source === "page" && hasEditableEval(plan.page.content, ev.name)) {
805
+ const updated = updatePageEval(
806
+ readFileSync2(plan.page.absPath, "utf8"),
807
+ plan.page.file,
808
+ ev.name,
809
+ updates
810
+ );
811
+ writeFileSync3(plan.page.absPath, updated);
812
+ proposal.applied = true;
813
+ } else if (ev.source === "config") {
814
+ const updated = updateConfigEval(
815
+ readFileSync2(config.configPath, "utf8"),
816
+ config.configPath,
817
+ ev.name,
818
+ updates
819
+ );
820
+ writeFileSync3(config.configPath, updated);
821
+ proposal.applied = true;
822
+ }
823
+ }
824
+ proposals.push(proposal);
825
+ }
826
+ }
827
+ return proposals;
828
+ }
829
+
830
+ // src/docevals/commands/review.ts
831
+ import { existsSync as existsSync2 } from "node:fs";
832
+ import { resolve as resolve2, relative as relative2 } from "node:path";
833
+ function listReviews(cwd = process.cwd()) {
834
+ return loadReviews(cwd);
835
+ }
836
+ function renderReviews(reviews, opts = {}) {
837
+ if (reviews.length === 0) return "No recorded reviews.";
838
+ const pc = palette(opts.color === true);
839
+ return reviews.map((r) => {
840
+ const verdict = r.verdict === "pass" ? pc.green(r.verdict) : pc.red(r.verdict);
841
+ const meta = [r.reviewer, r.date].filter(Boolean).join(" ");
842
+ return `${verdict} ${r.file} ${pc.bold(r.evalName)}${meta ? pc.dim(` (${meta})`) : ""}${r.note ? pc.dim(` \u2014 ${r.note}`) : ""}`;
843
+ }).join("\n");
844
+ }
845
+ function runReview(file, evalName, verdict, options = {}) {
846
+ const cwd = options.cwd ?? process.cwd();
847
+ if (verdict !== "pass" && verdict !== "fail") {
848
+ throw new DocevalsError(`Verdict must be "pass" or "fail", got "${verdict}"`);
849
+ }
850
+ const absPath = resolve2(cwd, file);
851
+ if (!existsSync2(absPath)) {
852
+ throw new DocevalsError(`Page not found: ${absPath}`);
853
+ }
854
+ const page = readPage(absPath, cwd);
855
+ const entry = {
856
+ file: relative2(cwd, absPath).replace(/\\/g, "/"),
857
+ evalName,
858
+ contentHash: sha256(page.body),
859
+ verdict,
860
+ reviewer: options.reviewer,
861
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
862
+ note: options.note
863
+ };
864
+ recordReview(cwd, entry);
865
+ return entry;
866
+ }
867
+
868
+ // src/docevals/commands/calibrate.ts
869
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
870
+ import { join as join2, resolve as resolve3 } from "node:path";
871
+ import fg from "fast-glob";
872
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
873
+ var AGREEMENT_THRESHOLD = 0.7;
874
+ var GOLDEN_DIR = join2(".manni/docevals", "golden");
875
+ var SEEDED_GOLDEN_FILE = "from-reviews.yaml";
876
+ function normalizeGoldenCase(raw) {
877
+ return {
878
+ file: raw.file,
879
+ eval: raw.eval,
880
+ expected: raw.expected,
881
+ ...typeof raw.rationale === "string" ? { rationale: raw.rationale } : {},
882
+ reviewed: raw.reviewed === true,
883
+ ...typeof raw["content-hash"] === "string" ? { contentHash: raw["content-hash"] } : {},
884
+ ...raw.source === "review" || raw.source === "manual" ? { source: raw.source } : {},
885
+ ...typeof raw["reviewed-by"] === "string" ? { reviewedBy: raw["reviewed-by"] } : {}
886
+ };
887
+ }
888
+ function serializeGoldenCase(c) {
889
+ return {
890
+ file: c.file,
891
+ eval: c.eval,
892
+ expected: c.expected,
893
+ ...c.rationale === void 0 ? {} : { rationale: c.rationale },
894
+ reviewed: c.reviewed,
895
+ ...c.contentHash === void 0 ? {} : { "content-hash": c.contentHash },
896
+ ...c.source === void 0 ? {} : { source: c.source },
897
+ ...c.reviewedBy === void 0 ? {} : { "reviewed-by": c.reviewedBy }
898
+ };
899
+ }
900
+ function loadGoldenCases(dir) {
901
+ if (!existsSync3(dir)) {
902
+ throw new DocevalsError(
903
+ `Golden set directory not found: ${dir} \u2014 seed it from recorded reviews with \`manni docevals calibrate --seed\`, or hand-author 20-50 verified cases`
904
+ );
905
+ }
906
+ const files = fg.sync("*.{yaml,yml}", { cwd: dir, absolute: true });
907
+ const cases = [];
908
+ for (const file of files) {
909
+ const raw = parseYaml(readFileSync3(file, "utf8"));
910
+ if (!Array.isArray(raw)) continue;
911
+ for (const item of raw) {
912
+ const c = item;
913
+ if (typeof c.file === "string" && typeof c.eval === "string" && (c.expected === "pass" || c.expected === "fail")) {
914
+ cases.push(normalizeGoldenCase(c));
915
+ } else {
916
+ throw new DocevalsError(
917
+ `Invalid golden case in ${file}: needs file, eval, expected: pass|fail`
918
+ );
919
+ }
920
+ }
921
+ }
922
+ if (cases.length === 0) {
923
+ throw new DocevalsError(`No golden cases found in ${dir}`);
924
+ }
925
+ return cases;
926
+ }
927
+ function seedGoldenCases(options = {}) {
928
+ const cwd = options.cwd ?? process.cwd();
929
+ const dir = resolve3(cwd, options.golden ?? GOLDEN_DIR);
930
+ const path = join2(dir, SEEDED_GOLDEN_FILE);
931
+ const priorRaw = existsSync3(path) ? parseYaml(readFileSync3(path, "utf8")) : null;
932
+ const existing = Array.isArray(priorRaw) ? priorRaw.filter((i) => typeof i === "object" && i !== null).map(normalizeGoldenCase) : [];
933
+ const byKey = new Map(existing.map((c) => [`${c.file}\0${c.eval}`, c]));
934
+ let added = 0;
935
+ let updated = 0;
936
+ for (const review of loadReviews(cwd)) {
937
+ const key = `${review.file}\0${review.evalName}`;
938
+ const prior = byKey.get(key);
939
+ const confirmedStillApplies = prior?.reviewed === true && prior.expected === review.verdict;
940
+ const next = {
941
+ file: review.file,
942
+ eval: review.evalName,
943
+ expected: review.verdict,
944
+ // A review's note wins when it has one; otherwise keep whatever the
945
+ // human wrote into the file. Rebuilding the entry from the review alone
946
+ // erased the reasoning behind a case the same human had just marked
947
+ // `reviewed: true` -- and it is the field renderCalibration prints on
948
+ // every disagreement.
949
+ ...review.note !== void 0 ? { rationale: review.note } : prior?.rationale !== void 0 ? { rationale: prior.rationale } : {},
950
+ reviewed: confirmedStillApplies,
951
+ contentHash: review.contentHash,
952
+ source: "review",
953
+ ...confirmedStillApplies && prior.reviewedBy !== void 0 ? { reviewedBy: prior.reviewedBy } : {}
954
+ };
955
+ byKey.set(key, next);
956
+ if (!prior) added += 1;
957
+ else if (JSON.stringify(serializeGoldenCase(prior)) !== JSON.stringify(serializeGoldenCase(next))) {
958
+ updated += 1;
959
+ }
960
+ }
961
+ const cases = [...byKey.values()];
962
+ if (cases.length > 0) {
963
+ mkdirSync3(dir, { recursive: true });
964
+ writeFileSync4(path, stringifyYaml(cases.map(serializeGoldenCase)));
965
+ }
966
+ return {
967
+ path,
968
+ added,
969
+ updated,
970
+ total: cases.length,
971
+ unreviewed: cases.filter((c) => !c.reviewed).length
972
+ };
973
+ }
974
+ async function runCalibrate(options = {}) {
975
+ const cwd = options.cwd ?? process.cwd();
976
+ const config = loadConfig(options.config, cwd);
977
+ const flags = { provider: options.provider, model: options.model, local: options.local };
978
+ assertProviderSelection(selectProvider(config, flags));
979
+ const goldenDir = resolve3(cwd, options.golden ?? GOLDEN_DIR);
980
+ const cases = loadGoldenCases(goldenDir);
981
+ const results = [];
982
+ const targets = [];
983
+ const targetIndex = [];
984
+ const external = await loadExternalReader(
985
+ config,
986
+ cwd,
987
+ cases.map((c) => resolve3(cwd, c.file)).filter((p) => existsSync3(p))
988
+ );
989
+ for (const goldenCase of cases) {
990
+ const absPath = resolve3(cwd, goldenCase.file);
991
+ if (!existsSync3(absPath)) {
992
+ results.push({ ...goldenCase, error: "page not found" });
993
+ continue;
994
+ }
995
+ const read = readPage(absPath, cwd);
996
+ const page = external === null ? read : await external.forPage(read);
997
+ const plan = resolvePage(page, config);
998
+ const ev = plan.evals.find(
999
+ (e) => e.name === goldenCase.eval && e.grader === "ai"
1000
+ );
1001
+ if (!ev) {
1002
+ results.push({
1003
+ ...goldenCase,
1004
+ error: `ai-graded eval "${goldenCase.eval}" not resolvable on this page`
1005
+ });
1006
+ continue;
1007
+ }
1008
+ const stale = goldenCase.contentHash !== void 0 && goldenCase.contentHash !== sha256(plan.page.body);
1009
+ targetIndex.push(results.length);
1010
+ results.push({ ...goldenCase, ...stale ? { stale } : {} });
1011
+ targets.push({ plan, eval: ev });
1012
+ }
1013
+ if (targets.length > 0) {
1014
+ const judge = options.judge ?? makeJudge({
1015
+ provider: await makeProvider(config, flags),
1016
+ root: cwd
1017
+ });
1018
+ const judged = await judge(targets, config, {
1019
+ // The flags reach the judge too, so a golden case whose eval names its
1020
+ // own provider or model is overridden by them as it is under `run`.
1021
+ ...flags,
1022
+ runs: options.runs,
1023
+ noCache: options.noCache,
1024
+ maxTurns: options.maxTurns
1025
+ });
1026
+ for (const [i, at] of targetIndex.entries()) {
1027
+ const slot = results[at];
1028
+ if (slot === void 0) {
1029
+ throw new DocevalsError(
1030
+ `calibrate: no golden case at index ${at} for judged target ${i}`
1031
+ );
1032
+ }
1033
+ const consensus = judged[i]?.consensus;
1034
+ if (!consensus) {
1035
+ const reason = judged[i]?.skipReason;
1036
+ const fromBudget = isTurnBudgetSkip(reason);
1037
+ results[at] = {
1038
+ ...slot,
1039
+ ...fromBudget ? { budgetSkipped: true } : {},
1040
+ error: fromBudget ? `not judged: ${reason}` : "judge returned no consensus"
1041
+ };
1042
+ continue;
1043
+ }
1044
+ const verdict = consensus.verdict === "pass" ? "pass" : "fail";
1045
+ results[at] = {
1046
+ ...slot,
1047
+ judged: verdict,
1048
+ agrees: verdict === slot.expected
1049
+ };
1050
+ }
1051
+ }
1052
+ const judgedCases = results.filter((r) => r.judged != null);
1053
+ const agreements = judgedCases.filter((r) => r.agrees).length;
1054
+ const expectedPass = judgedCases.filter((r) => r.expected === "pass");
1055
+ const falsePositives = expectedPass.filter((r) => r.judged === "fail").length;
1056
+ const expectedFailCases = judgedCases.filter((r) => r.expected === "fail");
1057
+ const falseNegatives = expectedFailCases.filter(
1058
+ (r) => r.judged === "pass"
1059
+ ).length;
1060
+ const agreementRate = judgedCases.length > 0 ? agreements / judgedCases.length : 0;
1061
+ const falsePositiveRate = expectedPass.length > 0 ? falsePositives / expectedPass.length : 0;
1062
+ const budgetSkipped = results.filter((r) => r.budgetSkipped === true).length;
1063
+ const unjudged = results.length - judgedCases.length;
1064
+ return {
1065
+ cases: results,
1066
+ budgetSkipped,
1067
+ unjudged,
1068
+ unreviewed: judgedCases.filter((r) => !r.reviewed).length,
1069
+ stale: judgedCases.filter((r) => r.stale === true).length,
1070
+ total: results.length,
1071
+ agreements,
1072
+ agreementRate,
1073
+ falsePositives,
1074
+ falsePositiveRate,
1075
+ falseNegatives,
1076
+ meetsThreshold: agreementRate >= AGREEMENT_THRESHOLD,
1077
+ expectedPass: expectedPass.length,
1078
+ expectedFail: expectedFailCases.length,
1079
+ balanced: expectedPass.length > 0 && expectedFailCases.length > 0,
1080
+ fpAlert: falsePositiveRate > config.judge.falsePositiveAlert
1081
+ };
1082
+ }
1083
+ function renderCalibration(report, opts = {}) {
1084
+ const pc = palette(opts.color === true);
1085
+ const lines = [];
1086
+ for (const c of report.cases) {
1087
+ if (c.error) {
1088
+ lines.push(`${pc.red("error")} ${c.file} ${c.eval}: ${c.error}`);
1089
+ continue;
1090
+ }
1091
+ const tag = c.agrees ? pc.green("agree") : pc.red("DISAGREE");
1092
+ const flags = [
1093
+ c.reviewed ? "" : pc.yellow(" [unreviewed]"),
1094
+ c.stale ? pc.yellow(" [stale]") : ""
1095
+ ].join("");
1096
+ lines.push(
1097
+ `${tag} ${c.file} ${pc.bold(c.eval)}${flags}: judge=${c.judged ?? "(no verdict)"} human=${c.expected}` + (!c.agrees && c.rationale ? pc.dim(` \u2014 human: ${c.rationale}`) : "")
1098
+ );
1099
+ }
1100
+ lines.push("");
1101
+ const judged = report.cases.filter((c) => c.judged != null).length;
1102
+ lines.push(
1103
+ `Agreement: ${report.agreements}/${judged} judged (${(report.agreementRate * 100).toFixed(0)}%) \u2014 threshold ${(AGREEMENT_THRESHOLD * 100).toFixed(0)}%` + (judged === report.total ? "" : ` \u2014 ${report.total - judged} of ${report.total} case(s) not judged`)
1104
+ );
1105
+ lines.push(
1106
+ `False positives: ${report.falsePositives} (${(report.falsePositiveRate * 100).toFixed(0)}% of human-passes), false negatives: ${report.falseNegatives}`
1107
+ );
1108
+ if (report.unjudged > 0) {
1109
+ lines.push(
1110
+ pc.red(
1111
+ `
1112
+ ${report.unjudged} of ${report.total} case(s) never reached a verdict, so the rate above is over a sample. A calibration measured on part of the set does not certify the judge.`
1113
+ )
1114
+ );
1115
+ }
1116
+ if (judged > 0 && !report.balanced) {
1117
+ const missing = report.expectedFail === 0 ? "fail" : "pass";
1118
+ const always = missing === "fail" ? "pass" : "fail";
1119
+ lines.push(
1120
+ pc.red(
1121
+ `
1122
+ No \`expected: ${missing}\` cases among the ${judged} judged. A judge that answered "${always}" every time would score 100% here, so this run cannot certify anything. Add at least one case the judge should ${missing === "fail" ? "reject" : "accept"}.`
1123
+ )
1124
+ );
1125
+ }
1126
+ if (judged > 0 && !report.meetsThreshold) {
1127
+ lines.push(
1128
+ pc.red(
1129
+ "\nAgreement is below threshold. Refine the eval criteria first \u2014 make assertions more specific \u2014 before changing the grading mechanism."
1130
+ )
1131
+ );
1132
+ }
1133
+ if (report.budgetSkipped > 0) {
1134
+ lines.push(
1135
+ pc.red(
1136
+ `
1137
+ ${report.budgetSkipped} case(s) were never judged: the turn budget ran out. A rate measured over the rest is not a calibration \u2014 raise --max-turns and re-run.`
1138
+ )
1139
+ );
1140
+ }
1141
+ if (report.fpAlert) {
1142
+ lines.push(
1143
+ pc.yellow(
1144
+ "\nFalse-positive rate exceeds judge.falsePositiveAlert \u2014 the judge is flagging content humans accept. Consider tightening assertions or examples."
1145
+ )
1146
+ );
1147
+ }
1148
+ if (report.unreviewed > 0) {
1149
+ lines.push(
1150
+ pc.yellow(
1151
+ `
1152
+ ${report.unreviewed} of ${report.total} case(s) are unreviewed \u2014 they count toward the rate above, but no human has confirmed they belong in the golden set. Read them and set \`reviewed: true\`.`
1153
+ )
1154
+ );
1155
+ }
1156
+ if (report.stale > 0) {
1157
+ lines.push(
1158
+ pc.yellow(
1159
+ `
1160
+ ${report.stale} case(s) are stale: the page changed since the verdict was recorded, so the case describes a document that no longer exists. Re-verify and update \`content-hash\`.`
1161
+ )
1162
+ );
1163
+ }
1164
+ return lines.join("\n");
1165
+ }
1166
+
1167
+ // src/docevals/commands/init.ts
1168
+ import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
1169
+ import { resolve as resolve4 } from "node:path";
1170
+ var STARTER_CONFIG = `# manni.config.yaml \u2014 shared configuration for the manni family of tools.
1171
+ # Each tool reads its own top-level key; manni docevals reads "docevals:".
1172
+ # Docs: https://hawkeyexl.github.io/manni/docevals/
1173
+
1174
+ # The documents every manni tool reads, declared once. A bare
1175
+ # \`manni docevals run\` evaluates every collection listed here; pass
1176
+ # --collection <name> to pick one, or name paths on the command line instead.
1177
+ # node_modules and .git are never read.
1178
+ collections:
1179
+ - name: site
1180
+ paths:
1181
+ - "docs/**/*.{md,mdx}"
1182
+
1183
+ # The inference provider, declared once for every manni tool that sends
1184
+ # content to a model: manni docevals and manni meta fill. auto detects what
1185
+ # this machine can use: an Anthropic key, then an OpenAI key, then the Claude
1186
+ # CLI, then a local model. Name one to pin it. A tool's own provider key, or
1187
+ # --provider, still wins for that tool.
1188
+ providers:
1189
+ provider: auto # auto | anthropic | openai | claude-cli | llama-cpp
1190
+ # model: <id> # needs a named provider; unset takes that provider's default
1191
+ # anthropic: { apiKeyEnv: ANTHROPIC_API_KEY }
1192
+ # openai: { baseUrl: http://localhost:11434/v1, apiKeyEnv: OPENAI_API_KEY }
1193
+ # claude-cli: { command: claude } # uses local CLI auth, no API key
1194
+ # llama-cpp: { modelsDir: .manni/models, thoughtTokens: 0 } # modelsDir is relative to this file
1195
+
1196
+ docevals:
1197
+ defaults:
1198
+ # Suite applied to pages without an eval-suite frontmatter key. Naming the
1199
+ # suite defined at the bottom of this file is what makes a fresh corpus
1200
+ # check anything at all: with "null", a page carrying no eval frontmatter
1201
+ # resolves zero evals, and a run over zero evals is a usage error rather
1202
+ # than a green build (ADR 01041).
1203
+ suite: default
1204
+ failFast: false
1205
+ concurrency: 4
1206
+
1207
+ judge:
1208
+ ensembleRuns: 3 # 3 isolated runs per eval; agreement is signal
1209
+ # NOTE: defaults.suite attaches an ai eval to every discovered page, so a
1210
+ # keyed run on a large corpus issues ensembleRuns x pages requests. Set
1211
+ # maxTurns once you know what a full pass costs you \u2014 deliberately, and
1212
+ # high enough to cover the corpus. A budget set *below* what a full pass
1213
+ # needs stops early, reports the remaining pages as skipped, and still exits
1214
+ # 0 (ADR 01019): partial coverage that reads as success. Start with
1215
+ # --deterministic-only, which needs no provider at all.
1216
+ temperature: 0
1217
+ zones:
1218
+ autoPass: 0.8 # unanimous pass + mean confidence >= 0.8
1219
+ autoFail: 0.8
1220
+ falsePositiveAlert: 0.15
1221
+ cacheDir: .manni/docevals/cache
1222
+
1223
+ execution:
1224
+ # Every grant holds unless this list narrows it. frontmatter-commands lets
1225
+ # command evals declared in page frontmatter run. Write [] if you do not
1226
+ # trust whoever can edit this corpus's pages.
1227
+ allow: [frontmatter-commands]
1228
+
1229
+ scripts:
1230
+ dir: "{docDir}/manni-docevals" # generated check scripts live beside the docs
1231
+ configDir: manni-docevals-scripts
1232
+
1233
+ evals:
1234
+ no-future-promises:
1235
+ type: regression
1236
+ assertion: The page makes no claims about unreleased or future functionality.
1237
+ grader: ai
1238
+ evidence: All prose sections
1239
+ examples:
1240
+ pass: Describes only shipped behavior.
1241
+ fail: Says "coming soon" or references an unreleased version.
1242
+ no-todo-markers:
1243
+ assertion: The page carries no TODO, TBD or FIXME markers.
1244
+ grader: tool:regex
1245
+ options:
1246
+ pattern: "\\\\b(TODO|TBD|FIXME)\\\\b"
1247
+ match: not-contains
1248
+ severity: error
1249
+
1250
+ suites:
1251
+ default:
1252
+ target-pass-rate: 1.0 # regression suites target ~100%
1253
+ evals: [no-future-promises, no-todo-markers]
1254
+ `;
1255
+ function runInit(cwd = process.cwd()) {
1256
+ const path = resolve4(cwd, DEFAULT_CONFIG_FILENAME);
1257
+ if (existsSync4(path)) {
1258
+ throw new DocevalsError(`${DEFAULT_CONFIG_FILENAME} already exists`);
1259
+ }
1260
+ writeFileSync5(path, STARTER_CONFIG);
1261
+ return path;
1262
+ }
1263
+
1264
+ export {
1265
+ runGenerate,
1266
+ FILL_PROMPT_VERSION,
1267
+ FILL_SYSTEM_PROMPT,
1268
+ PROPOSAL_SCHEMA,
1269
+ isValidProposal,
1270
+ buildFillUser,
1271
+ fillCacheKey,
1272
+ FillCache,
1273
+ runFill,
1274
+ renderFill,
1275
+ runPromote,
1276
+ listReviews,
1277
+ renderReviews,
1278
+ runReview,
1279
+ GOLDEN_DIR,
1280
+ SEEDED_GOLDEN_FILE,
1281
+ loadGoldenCases,
1282
+ seedGoldenCases,
1283
+ runCalibrate,
1284
+ renderCalibration,
1285
+ runInit
1286
+ };
1287
+ //# sourceMappingURL=chunk-77HX34KD.js.map