@hawkeyexl/manni 4.3.1 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/batch-GFNLTGOH.js +36 -0
- package/dist/build-COYIQ25L.js +31 -0
- package/dist/calibrate-MDBLJKEE.js +31 -0
- package/dist/capture-LRML2PKV.js +32 -0
- package/dist/check-ASPIHA6B.js +40 -0
- package/dist/check-ASPIHA6B.js.map +1 -0
- package/dist/check-EDLBYL5N.js +27 -0
- package/dist/check-EDLBYL5N.js.map +1 -0
- package/dist/check-HYNGXYJV.js +18 -0
- package/dist/check-HYNGXYJV.js.map +1 -0
- package/dist/chunk-2B7ZNTPV.js +230 -0
- package/dist/chunk-2B7ZNTPV.js.map +1 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-37LP5HMZ.js +1042 -0
- package/dist/chunk-37LP5HMZ.js.map +1 -0
- package/dist/chunk-3VK37OR2.js +2957 -0
- package/dist/chunk-3VK37OR2.js.map +1 -0
- package/dist/chunk-4IUC2MNL.js +2165 -0
- package/dist/chunk-4IUC2MNL.js.map +1 -0
- package/dist/{chunk-3R23C5DT.js → chunk-4TSWU2SU.js} +2 -14
- package/dist/chunk-4TSWU2SU.js.map +1 -0
- package/dist/chunk-57NEQRVO.js +10921 -0
- package/dist/chunk-57NEQRVO.js.map +1 -0
- package/dist/chunk-6JSSEWBG.js +6737 -0
- package/dist/chunk-6JSSEWBG.js.map +1 -0
- package/dist/chunk-6OYEI2VV.js +88 -0
- package/dist/chunk-6OYEI2VV.js.map +1 -0
- package/dist/chunk-77AINBYM.js +19 -0
- package/dist/chunk-77AINBYM.js.map +1 -0
- package/dist/chunk-7UKO3ASZ.js +54 -0
- package/dist/chunk-7UKO3ASZ.js.map +1 -0
- package/dist/chunk-A5ULMVHQ.js +164 -0
- package/dist/chunk-A5ULMVHQ.js.map +1 -0
- package/dist/chunk-C4SQM22E.js +299 -0
- package/dist/chunk-C4SQM22E.js.map +1 -0
- package/dist/chunk-CK7SKVHR.js +1042 -0
- package/dist/chunk-CK7SKVHR.js.map +1 -0
- package/dist/{chunk-G5CM2VGQ.js → chunk-DXNZS72W.js} +8 -94
- package/dist/chunk-DXNZS72W.js.map +1 -0
- package/dist/chunk-EPH54CAY.js +374 -0
- package/dist/chunk-EPH54CAY.js.map +1 -0
- package/dist/{chunk-KJR7OVDX.js → chunk-FBEQCK47.js} +56 -137
- package/dist/chunk-FBEQCK47.js.map +1 -0
- package/dist/{chunk-IOYXPBCL.js → chunk-G3T47CFV.js} +1 -7
- package/dist/{chunk-IOYXPBCL.js.map → chunk-G3T47CFV.js.map} +1 -1
- package/dist/chunk-GCQRPUSB.js +1287 -0
- package/dist/chunk-GCQRPUSB.js.map +1 -0
- package/dist/chunk-GLP5PSXB.js +72 -0
- package/dist/chunk-GLP5PSXB.js.map +1 -0
- package/dist/chunk-I2AH3CJJ.js +1192 -0
- package/dist/chunk-I2AH3CJJ.js.map +1 -0
- package/dist/chunk-IDV6HJCB.js +388 -0
- package/dist/chunk-IDV6HJCB.js.map +1 -0
- package/dist/chunk-IKEWBMCN.js +98 -0
- package/dist/chunk-IKEWBMCN.js.map +1 -0
- package/dist/chunk-JCDXE76J.js +52 -0
- package/dist/chunk-JCDXE76J.js.map +1 -0
- package/dist/chunk-JXLTXMJ7.js +455 -0
- package/dist/chunk-JXLTXMJ7.js.map +1 -0
- package/dist/chunk-LJ2RDH3F.js +49 -0
- package/dist/chunk-LJ2RDH3F.js.map +1 -0
- package/dist/chunk-M2JCK5F6.js +805 -0
- package/dist/chunk-M2JCK5F6.js.map +1 -0
- package/dist/chunk-M3W7J6MU.js +4398 -0
- package/dist/chunk-M3W7J6MU.js.map +1 -0
- package/dist/chunk-MMTSSTG7.js +23 -0
- package/dist/chunk-MMTSSTG7.js.map +1 -0
- package/dist/chunk-MNH7QO4Z.js +15 -0
- package/dist/chunk-MNH7QO4Z.js.map +1 -0
- package/dist/chunk-MWJLZMBL.js +934 -0
- package/dist/chunk-MWJLZMBL.js.map +1 -0
- package/dist/chunk-MYMBCTDE.js +387 -0
- package/dist/chunk-MYMBCTDE.js.map +1 -0
- package/dist/chunk-NCPWXHDO.js +683 -0
- package/dist/chunk-NCPWXHDO.js.map +1 -0
- package/dist/chunk-NLXTXE5Y.js +1661 -0
- package/dist/chunk-NLXTXE5Y.js.map +1 -0
- package/dist/{chunk-CFDUZRM2.js → chunk-NXV7WE2D.js} +32 -240
- package/dist/chunk-NXV7WE2D.js.map +1 -0
- package/dist/chunk-OWBRDNRZ.js +16 -0
- package/dist/chunk-OWBRDNRZ.js.map +1 -0
- package/dist/{chunk-KTSJ2ZHA.js → chunk-PAU352SV.js} +5 -3
- package/dist/{chunk-KTSJ2ZHA.js.map → chunk-PAU352SV.js.map} +1 -1
- package/dist/chunk-PCE7QHXJ.js +68 -0
- package/dist/chunk-PCE7QHXJ.js.map +1 -0
- package/dist/chunk-PIKOVN6W.js +1651 -0
- package/dist/chunk-PIKOVN6W.js.map +1 -0
- package/dist/chunk-PNPQTHBT.js +17 -0
- package/dist/chunk-PNPQTHBT.js.map +1 -0
- package/dist/chunk-PU6DFJMY.js +1975 -0
- package/dist/chunk-PU6DFJMY.js.map +1 -0
- package/dist/chunk-Q35YIDBV.js +1202 -0
- package/dist/chunk-Q35YIDBV.js.map +1 -0
- package/dist/{chunk-5HNHEUD5.js → chunk-QQUKGCAJ.js} +3 -3
- package/dist/chunk-QRWGPQKO.js +198 -0
- package/dist/chunk-QRWGPQKO.js.map +1 -0
- package/dist/{chunk-UBMMXS2Y.js → chunk-QZ6LRSNV.js} +69 -48
- package/dist/{chunk-UBMMXS2Y.js.map → chunk-QZ6LRSNV.js.map} +1 -1
- package/dist/chunk-RL5XWQOL.js +3872 -0
- package/dist/chunk-RL5XWQOL.js.map +1 -0
- package/dist/chunk-RNVYGWIC.js +30 -0
- package/dist/chunk-RNVYGWIC.js.map +1 -0
- package/dist/chunk-SE4JN5XN.js +19 -0
- package/dist/chunk-SE4JN5XN.js.map +1 -0
- package/dist/chunk-SKSDRUUK.js +622 -0
- package/dist/chunk-SKSDRUUK.js.map +1 -0
- package/dist/chunk-TIUTIOFB.js +25 -0
- package/dist/chunk-TIUTIOFB.js.map +1 -0
- package/dist/chunk-TK3FC4RF.js +3090 -0
- package/dist/chunk-TK3FC4RF.js.map +1 -0
- package/dist/chunk-TU2WUBVZ.js +2 -0
- package/dist/chunk-TU2WUBVZ.js.map +1 -0
- package/dist/chunk-TZOYZK2E.js +98 -0
- package/dist/chunk-TZOYZK2E.js.map +1 -0
- package/dist/chunk-UJCVAWP7.js +178 -0
- package/dist/chunk-UJCVAWP7.js.map +1 -0
- package/dist/chunk-UKZNLVJW.js +17 -0
- package/dist/chunk-UKZNLVJW.js.map +1 -0
- package/dist/{chunk-HAPTXWU2.js → chunk-UQISLDAV.js} +4 -4
- package/dist/chunk-WB3QM6PP.js +5440 -0
- package/dist/chunk-WB3QM6PP.js.map +1 -0
- package/dist/chunk-YB4IBW3M.js +96 -0
- package/dist/chunk-YB4IBW3M.js.map +1 -0
- package/dist/chunk-YEUYKR62.js +72 -0
- package/dist/chunk-YEUYKR62.js.map +1 -0
- package/dist/chunk-YJOGUXGP.js +634 -0
- package/dist/chunk-YJOGUXGP.js.map +1 -0
- package/dist/chunk-YNHZHSSM.js +41 -0
- package/dist/chunk-YNHZHSSM.js.map +1 -0
- package/dist/chunk-YTURO7SI.js +979 -0
- package/dist/chunk-YTURO7SI.js.map +1 -0
- package/dist/chunk-ZXRAE55H.js +31 -0
- package/dist/chunk-ZXRAE55H.js.map +1 -0
- package/dist/cli-3BIBYHFK.js +473 -0
- package/dist/cli-3BIBYHFK.js.map +1 -0
- package/dist/cli-673OTRGE.js +28 -0
- package/dist/cli-673OTRGE.js.map +1 -0
- package/dist/cli-6HIDIFVT.js +1203 -0
- package/dist/cli-6HIDIFVT.js.map +1 -0
- package/dist/cli-6UB5N6CU.js +232 -0
- package/dist/cli-6UB5N6CU.js.map +1 -0
- package/dist/cli-LFP5ZGYS.js +346 -0
- package/dist/cli-LFP5ZGYS.js.map +1 -0
- package/dist/cli-MN7PYMBI.js +552 -0
- package/dist/cli-MN7PYMBI.js.map +1 -0
- package/dist/cli-PYPX6O6E.js +739 -0
- package/dist/cli-PYPX6O6E.js.map +1 -0
- package/dist/cli-RPM5PELX.js +626 -0
- package/dist/cli-RPM5PELX.js.map +1 -0
- package/dist/cli-VI7VHWNZ.js +423 -0
- package/dist/cli-VI7VHWNZ.js.map +1 -0
- package/dist/cli.d.ts +12 -2
- package/dist/cli.js +109 -7661
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +15 -3
- package/dist/docmeta.js.map +1 -1
- package/dist/github-6QMYOHC3.js +27 -0
- package/dist/github-6QMYOHC3.js.map +1 -0
- package/dist/github-SXPGP5DB.js +32 -0
- package/dist/github-SXPGP5DB.js.map +1 -0
- package/dist/index.d.ts +65 -3
- package/dist/index.js +407 -337
- package/dist/index.js.map +1 -1
- package/dist/json-7WV7KL6H.js +17 -0
- package/dist/json-7WV7KL6H.js.map +1 -0
- package/dist/json-SY7KLNC7.js +35 -0
- package/dist/json-SY7KLNC7.js.map +1 -0
- package/dist/lint-ASMJLLPA.js +28 -0
- package/dist/lint-ASMJLLPA.js.map +1 -0
- package/dist/list-KZNVTNVC.js +31 -0
- package/dist/list-KZNVTNVC.js.map +1 -0
- package/dist/list-Q57YTFUQ.js +29 -0
- package/dist/list-Q57YTFUQ.js.map +1 -0
- package/dist/page-KSGV5RD4.js +41 -0
- package/dist/page-KSGV5RD4.js.map +1 -0
- package/dist/picker-3XF36MJF.js +14 -0
- package/dist/picker-3XF36MJF.js.map +1 -0
- package/dist/pretty-A5EGTN35.js +37 -0
- package/dist/pretty-A5EGTN35.js.map +1 -0
- package/dist/pretty-CXSDXNAS.js +43 -0
- package/dist/pretty-CXSDXNAS.js.map +1 -0
- package/dist/reporters-HUY7UXFS.js +42 -0
- package/dist/reporters-HUY7UXFS.js.map +1 -0
- package/dist/reporters-K2VUKSAK.js +71 -0
- package/dist/reporters-K2VUKSAK.js.map +1 -0
- package/dist/reporters-M6LJ2IYU.js +21 -0
- package/dist/reporters-M6LJ2IYU.js.map +1 -0
- package/dist/run-3DYJNRIG.js +33 -0
- package/dist/run-3DYJNRIG.js.map +1 -0
- package/dist/term-BIQUPMOR.js +1592 -0
- package/dist/term-BIQUPMOR.js.map +1 -0
- package/dist/types-Y6TXHM7M.js +10 -0
- package/dist/types-Y6TXHM7M.js.map +1 -0
- package/dist/validate-53RKM2XG.js +16 -0
- package/dist/validate-53RKM2XG.js.map +1 -0
- package/package.json +1 -1
- package/plugin/manni/.claude-plugin/plugin.json +9 -0
- package/plugin/manni/hooks/hooks.json +69 -0
- package/plugin/manni/hooks/manni.mjs +157 -0
- package/plugin/manni/skills/check/SKILL.md +20 -0
- package/plugin/manni/skills/evals/SKILL.md +27 -0
- package/plugin/manni/skills/fix/SKILL.md +56 -0
- package/plugin/manni/skills/setup/SKILL.md +41 -0
- package/dist/batch-A5HZKYIW.js +0 -18
- package/dist/calibrate-S5K6QHUY.js +0 -13
- package/dist/capture-OZEZDABQ.js +0 -15
- package/dist/chunk-3R23C5DT.js.map +0 -1
- package/dist/chunk-CFDUZRM2.js.map +0 -1
- package/dist/chunk-G5CM2VGQ.js.map +0 -1
- package/dist/chunk-KJR7OVDX.js.map +0 -1
- package/dist/chunk-V4KUVE47.js +0 -27170
- package/dist/chunk-V4KUVE47.js.map +0 -1
- package/dist/chunk-ZQBTK3DO.js +0 -26518
- package/dist/chunk-ZQBTK3DO.js.map +0 -1
- package/dist/picker-6HRMETE7.js +0 -12
- package/plugin/tracevals/.claude-plugin/plugin.json +0 -9
- package/plugin/tracevals/hooks/hooks.json +0 -17
- /package/dist/{batch-A5HZKYIW.js.map → batch-GFNLTGOH.js.map} +0 -0
- /package/dist/{calibrate-S5K6QHUY.js.map → build-COYIQ25L.js.map} +0 -0
- /package/dist/{capture-OZEZDABQ.js.map → calibrate-MDBLJKEE.js.map} +0 -0
- /package/dist/{picker-6HRMETE7.js.map → capture-LRML2PKV.js.map} +0 -0
- /package/dist/{chunk-5HNHEUD5.js.map → chunk-QQUKGCAJ.js.map} +0 -0
- /package/dist/{chunk-HAPTXWU2.js.map → chunk-UQISLDAV.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-37LP5HMZ.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-3VK37OR2.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-A5ULMVHQ.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-GCQRPUSB.js.map
|