@aiwg/cli 2026.10.5 → 2026.10.7

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 (263) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +7 -3
  6. package/dist/src/a2a/agent-card.js +11 -0
  7. package/dist/src/api/index.d.ts +1 -0
  8. package/dist/src/api/index.js +1 -0
  9. package/dist/src/artifacts/browser-export.js +1 -0
  10. package/dist/src/artifacts/citation-parser.js +8 -1
  11. package/dist/src/artifacts/cli.js +57 -6
  12. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  13. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  14. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  15. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  16. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  17. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  18. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  19. package/dist/src/artifacts/graph-query.js +18 -8
  20. package/dist/src/artifacts/index-adapters.js +339 -0
  21. package/dist/src/artifacts/index-builder.js +143 -14
  22. package/dist/src/artifacts/index-files.js +15 -4
  23. package/dist/src/artifacts/index-preparation.js +2 -0
  24. package/dist/src/artifacts/index-reader.js +44 -0
  25. package/dist/src/artifacts/index-refresh.js +114 -55
  26. package/dist/src/artifacts/index-status.js +20 -5
  27. package/dist/src/artifacts/memory-maintenance.js +503 -0
  28. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  29. package/dist/src/artifacts/query-engine.js +263 -108
  30. package/dist/src/artifacts/relations-builder.js +2 -2
  31. package/dist/src/artifacts/source-graph.js +2 -2
  32. package/dist/src/artifacts/types.js +9 -1
  33. package/dist/src/auth/client.js +5 -1
  34. package/dist/src/auth/credential-store.js +5 -1
  35. package/dist/src/auth/sensitive-input.js +151 -0
  36. package/dist/src/channel/manager.mjs +3 -0
  37. package/dist/src/cli/handlers/agentcard.js +47 -25
  38. package/dist/src/cli/handlers/auth.js +22 -2
  39. package/dist/src/cli/handlers/decision.js +22 -3
  40. package/dist/src/cli/handlers/effect.js +4 -84
  41. package/dist/src/cli/handlers/guided-setup.js +470 -0
  42. package/dist/src/cli/handlers/help.js +2 -2
  43. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  44. package/dist/src/cli/handlers/index.js +4 -2
  45. package/dist/src/cli/handlers/init.js +41 -6
  46. package/dist/src/cli/handlers/install.js +6 -0
  47. package/dist/src/cli/handlers/local-executor.js +5 -3
  48. package/dist/src/cli/handlers/marketplace.js +15 -4
  49. package/dist/src/cli/handlers/mc.js +509 -53
  50. package/dist/src/cli/handlers/models.js +32 -2
  51. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  52. package/dist/src/cli/handlers/rules.js +1 -1
  53. package/dist/src/cli/handlers/script-runner.js +73 -2
  54. package/dist/src/cli/handlers/serve.js +5 -3
  55. package/dist/src/cli/handlers/sessions.js +1 -0
  56. package/dist/src/cli/handlers/setup.js +432 -1
  57. package/dist/src/cli/handlers/skill-lint.js +1 -1
  58. package/dist/src/cli/handlers/subcommands.js +118 -8
  59. package/dist/src/cli/handlers/use.js +335 -107
  60. package/dist/src/cli/handlers/workspace.js +3 -1
  61. package/dist/src/cli/prompt-utils.js +81 -0
  62. package/dist/src/cli/router.js +16 -1
  63. package/dist/src/config/aiwg-config.js +42 -0
  64. package/dist/src/config/cli.js +7 -3
  65. package/dist/src/config/decision-config.js +14 -1
  66. package/dist/src/config/gitignore.js +4 -0
  67. package/dist/src/config/native-sandbox-warnings.js +25 -0
  68. package/dist/src/config/workspace.js +2 -1
  69. package/dist/src/contributors/validation.js +6 -5
  70. package/dist/src/decision/adapters/jev.js +20 -31
  71. package/dist/src/decision/admission.js +7 -3
  72. package/dist/src/decision/ask.js +6 -1
  73. package/dist/src/decision/benchmark.js +482 -0
  74. package/dist/src/decision/ledger.js +22 -3
  75. package/dist/src/decision/pricing.js +147 -0
  76. package/dist/src/decision/qualification/gate-evidence.js +23 -18
  77. package/dist/src/decision/qualification/load-staged.js +576 -0
  78. package/dist/src/decision/qualification/load.js +109 -33
  79. package/dist/src/decision/qualification/runner.js +12 -5
  80. package/dist/src/decision/review/service.js +6 -4
  81. package/dist/src/decision/review/validate.js +23 -2
  82. package/dist/src/decision/savings.js +8 -3
  83. package/dist/src/discovery/index.js +492 -0
  84. package/dist/src/discovery/interview.js +238 -0
  85. package/dist/src/discovery/plan-apply.js +567 -0
  86. package/dist/src/discovery/plan-store.js +171 -0
  87. package/dist/src/discovery/plan-types.js +2 -0
  88. package/dist/src/discovery/plan-validation.js +139 -0
  89. package/dist/src/discovery/types.js +2 -0
  90. package/dist/src/effects/host.js +91 -0
  91. package/dist/src/extensions/commands/definitions.js +49 -9
  92. package/dist/src/extensions/manifest.js +71 -44
  93. package/dist/src/extensions/project-local-discovery.js +1 -1
  94. package/dist/src/extensions/project-local-gitignore.js +2 -0
  95. package/dist/src/extensions/project-quickref.js +18 -17
  96. package/dist/src/extensions/validation.js +32 -29
  97. package/dist/src/housekeeper/daemon-config.js +148 -0
  98. package/dist/src/housekeeper/ipc.js +84 -0
  99. package/dist/src/housekeeper/lifecycle.js +148 -0
  100. package/dist/src/housekeeper/platform.js +35 -0
  101. package/dist/src/housekeeper/status.js +70 -0
  102. package/dist/src/housekeeper/units.js +114 -0
  103. package/dist/src/issues/cli.js +16 -5
  104. package/dist/src/issues/live.js +44 -12
  105. package/dist/src/jobs/flow.js +22 -21
  106. package/dist/src/lint/rule-activation.js +20 -5
  107. package/dist/src/lint/runner.js +1 -1
  108. package/dist/src/marketplace/exchange.js +18 -6
  109. package/dist/src/marketplace/provenance.js +3 -2
  110. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  111. package/dist/src/mcp/cli.mjs +22 -96
  112. package/dist/src/mcp/config-file.mjs +6 -2
  113. package/dist/src/mcp/config-layers.mjs +23 -7
  114. package/dist/src/mcp/grok-build-config.mjs +2 -1
  115. package/dist/src/mcp/muse-native-config.mjs +2 -1
  116. package/dist/src/mcp/profiles.js +18 -31
  117. package/dist/src/mcp/profiles.mjs +18 -32
  118. package/dist/src/mcp/registry.js +7 -27
  119. package/dist/src/mcp/registry.mjs +7 -28
  120. package/dist/src/mcp/server.mjs +34 -1
  121. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  122. package/dist/src/mcp/tools/command-run.mjs +3 -2
  123. package/dist/src/mcp/tools/decision.mjs +49 -48
  124. package/dist/src/mcp/tools/discovery.mjs +46 -21
  125. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  126. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  127. package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
  128. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  129. package/dist/src/memory/audit.js +719 -0
  130. package/dist/src/memory/canonical-context.js +392 -13
  131. package/dist/src/memory/canonical-maintenance.js +95 -0
  132. package/dist/src/memory/cli-result.js +22 -0
  133. package/dist/src/memory/cli.js +176 -0
  134. package/dist/src/memory/context-pack.js +57 -6
  135. package/dist/src/memory/cost.js +512 -0
  136. package/dist/src/memory/firewall-classification.d.mts +7 -0
  137. package/dist/src/memory/firewall-classification.mjs +32 -0
  138. package/dist/src/memory/index.js +8 -0
  139. package/dist/src/memory/line-maintenance.js +68 -0
  140. package/dist/src/memory/maintenance-cli.js +286 -0
  141. package/dist/src/memory/maintenance-contract.js +136 -0
  142. package/dist/src/memory/maintenance-index.js +18 -0
  143. package/dist/src/memory/maintenance-owner.js +2 -0
  144. package/dist/src/memory/maintenance-record.js +138 -0
  145. package/dist/src/memory/maintenance.js +254 -0
  146. package/dist/src/memory/project-registry.js +2 -1
  147. package/dist/src/memory/provider-memory-adapter.js +154 -0
  148. package/dist/src/memory/recommend-cli.js +83 -0
  149. package/dist/src/memory/recommend.js +269 -0
  150. package/dist/src/memory/scheduled.js +81 -0
  151. package/dist/src/memory/taxonomy.js +44 -0
  152. package/dist/src/mission-protocol/codecs.js +19 -9
  153. package/dist/src/mission-protocol/worker-status.js +91 -1
  154. package/dist/src/models/config-loader.js +11 -1
  155. package/dist/src/models/model-capabilities.v1.json +74 -50
  156. package/dist/src/models/provider-policy.js +125 -62
  157. package/dist/src/models/resolver.js +45 -8
  158. package/dist/src/models/worker-installation.d.mts +8 -1
  159. package/dist/src/models/worker-installation.mjs +231 -38
  160. package/dist/src/models/wrapper-route.js +1 -1
  161. package/dist/src/output-modes/registry.js +8 -7
  162. package/dist/src/packages/adapters/git.js +99 -23
  163. package/dist/src/packages/adapters/gitea.js +8 -27
  164. package/dist/src/packages/adapters/github.js +7 -25
  165. package/dist/src/packages/registry.js +4 -4
  166. package/dist/src/plugin/metadata-validator.js +1 -1
  167. package/dist/src/process/cli-command.d.mts +5 -0
  168. package/dist/src/process/cli-command.mjs +42 -0
  169. package/dist/src/providers/deploy-version.d.mts +1 -0
  170. package/dist/src/providers/deploy-version.mjs +17 -0
  171. package/dist/src/providers/provider-definitions.js +19 -16
  172. package/dist/src/providers/skill-ownership.d.mts +11 -2
  173. package/dist/src/providers/skill-ownership.mjs +35 -17
  174. package/dist/src/rules/activation.d.mts +11 -2
  175. package/dist/src/rules/activation.mjs +82 -6
  176. package/dist/src/rules/inventory.js +1 -1
  177. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  178. package/dist/src/schema/zod-validation.d.mts +6 -0
  179. package/dist/src/schema/zod-validation.mjs +50 -0
  180. package/dist/src/serve/executor-registry.js +297 -20
  181. package/dist/src/serve/executor-token.d.mts +1 -0
  182. package/dist/src/serve/executor-token.mjs +8 -0
  183. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  184. package/dist/src/serve/worker-invocation.js +124 -15
  185. package/dist/src/sessions/adapters/claude.js +19 -18
  186. package/dist/src/sessions/adapters/codex.js +10 -9
  187. package/dist/src/sessions/adapters/copilot.js +14 -13
  188. package/dist/src/sessions/adapters/cursor.js +19 -18
  189. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  190. package/dist/src/sessions/adapters/factory.js +11 -10
  191. package/dist/src/sessions/adapters/generic.js +32 -23
  192. package/dist/src/sessions/adapters/grokbot.js +35 -35
  193. package/dist/src/sessions/adapters/hermes.js +20 -19
  194. package/dist/src/sessions/adapters/muse.js +50 -49
  195. package/dist/src/sessions/adapters/omp.js +5 -4
  196. package/dist/src/sessions/adapters/openclaw.js +30 -29
  197. package/dist/src/sessions/adapters/opencode.js +26 -26
  198. package/dist/src/sessions/adapters/openhuman.js +24 -23
  199. package/dist/src/sessions/adapters/pi.js +5 -4
  200. package/dist/src/sessions/adapters/windsurf.js +8 -8
  201. package/dist/src/sessions/analytics.js +10 -9
  202. package/dist/src/sessions/candidates.js +7 -6
  203. package/dist/src/sessions/contracts.js +63 -53
  204. package/dist/src/sessions/output-registration.js +11 -10
  205. package/dist/src/sessions/workspace-discovery.js +3 -4
  206. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  207. package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
  208. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  209. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  210. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  211. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  212. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  213. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  214. package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
  215. package/dist/src/tracker/project-inputs.js +2 -0
  216. package/dist/src/update/checker.d.mts +4 -0
  217. package/dist/src/update/checker.mjs +14 -0
  218. package/dist/src/update/notifier.mjs +2 -4
  219. package/dist/src/writing/exemplar-selection.js +6 -6
  220. package/dist/src/writing/voice-evaluation.js +24 -23
  221. package/dist/src/writing/writer-migration.js +14 -13
  222. package/dist/src/writing/writer-profile.js +19 -18
  223. package/dist/src/writing/writing-brief.js +18 -17
  224. package/dist/src/writing/writing-receipt.js +47 -46
  225. package/package.json +5 -2
  226. package/schemas/decision/DecisionReview.schema.json +78 -1
  227. package/schemas/mission-v1.schema.json +3 -0
  228. package/tools/agents/deploy-agents.mjs +67 -39
  229. package/tools/agents/providers/antigravity.mjs +6 -1
  230. package/tools/agents/providers/base.mjs +24 -14
  231. package/tools/agents/providers/claude.mjs +6 -3
  232. package/tools/agents/providers/codex.mjs +54 -57
  233. package/tools/agents/providers/context-write.mjs +28 -0
  234. package/tools/agents/providers/copilot.mjs +13 -4
  235. package/tools/agents/providers/cursor.mjs +1 -1
  236. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  237. package/tools/agents/providers/factory.mjs +7 -1
  238. package/tools/agents/providers/grok-build.mjs +18 -2
  239. package/tools/agents/providers/grokbot.mjs +1 -1
  240. package/tools/agents/providers/hermes.mjs +12 -15
  241. package/tools/agents/providers/muse-hooks.mjs +21 -14
  242. package/tools/agents/providers/muse.mjs +7 -2
  243. package/tools/agents/providers/omp.mjs +31 -5
  244. package/tools/agents/providers/opencode.mjs +14 -5
  245. package/tools/agents/providers/openhuman.mjs +1 -1
  246. package/tools/agents/providers/pi.mjs +1 -1
  247. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  248. package/tools/agents/providers/rule-delivery.mjs +289 -80
  249. package/tools/agents/providers/warp.mjs +4 -1
  250. package/tools/agents/providers/windsurf.mjs +13 -3
  251. package/tools/cli/workspace-status.mjs +5 -0
  252. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  253. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  254. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  255. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  256. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  257. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  258. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  259. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  260. package/tools/ralph-external/process-monitor.mjs +49 -8
  261. package/tools/ralph-external/session-launcher.mjs +405 -35
  262. package/tools/skills/deploy-skills-codex.mjs +11 -7
  263. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -0,0 +1,482 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { askDecision } from './ask.js';
3
+ import { BUNDLED_JEV_PRICING, tokenCost, validTokenPrice } from './pricing.js';
4
+ import { decisionSavingsWeights, estimateDecisionSavings } from './savings.js';
5
+ /** Require explicit rates and complete usage; defaults are not benchmark evidence. */
6
+ function observationCost(sample, rates, baseline = false) {
7
+ if (sample.cost !== undefined)
8
+ return sample.cost.usd;
9
+ const usage = sample.usage;
10
+ if (!rates || !usage || usage.inputTokens === null || usage.cachedInputTokens)
11
+ return null;
12
+ if (usage.outputTokens === null && (baseline || rates.jev_output_per_mtok !== 0 || rates.jev_input_per_mtok === undefined))
13
+ return null;
14
+ if (baseline) {
15
+ if (rates.llm_per_mtok === undefined)
16
+ return null;
17
+ }
18
+ else {
19
+ if (rates.jev_model && rates.jev_model !== sample.model)
20
+ return null;
21
+ if (!(rates.jev_input_per_mtok !== undefined && rates.jev_output_per_mtok !== undefined) && rates.jev_per_mtok === undefined)
22
+ return null;
23
+ }
24
+ const weights = decisionSavingsWeights({ cost: baseline ? { jev_per_mtok: rates.llm_per_mtok } : rates });
25
+ const cost = estimateDecisionSavings(0, false, usage.inputTokens + (usage.outputTokens ?? 0), weights, baseline ? null : usage);
26
+ return -cost.cost_weighted_net_usd;
27
+ }
28
+ function validateRates(rates) {
29
+ if (rates && Object.entries(rates).some(([key, value]) => /per_mtok$/.test(key) && (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1_000_000)))
30
+ throw new Error('Invalid benchmark rates.');
31
+ }
32
+ export const BENCHMARK_PASS_COUNTS = [1, 3, 5, 10, 15, 20];
33
+ export const MAX_BENCHMARK_PASSES = 20;
34
+ /** Descriptive collection totals, not policy-selection evidence. Missing usage stays unknown. */
35
+ export function benchmarkComparison(capture, pricing) {
36
+ validateRates(pricing);
37
+ const money = (n) => n === null ? 'unknown' : `$${n.toFixed(8)}`;
38
+ const totals = (samples, baseline = false) => {
39
+ const input = samples.every(s => s.usage?.inputTokens != null) ? samples.reduce((sum, s) => sum + s.usage.inputTokens, 0) : null;
40
+ const output = samples.every(s => s.usage?.outputTokens != null) ? samples.reduce((sum, s) => sum + s.usage.outputTokens, 0) : null;
41
+ const cached = samples.every(s => s.usage?.cachedInputTokens != null) ? samples.reduce((sum, s) => sum + s.usage.cachedInputTokens, 0) : null;
42
+ const costs = samples.map(s => observationCost(s, pricing, baseline));
43
+ return { input, output, cached, cost: costs.some(c => c === null) ? null : costs.reduce((sum, c) => sum + c, 0),
44
+ latency: samples.length ? samples.reduce((sum, s) => sum + s.latencyMs, 0) / capture.rows.length : 0 };
45
+ };
46
+ const display = (n) => n === null ? 'unknown' : n.toLocaleString('en-US');
47
+ const baseline = totals(capture.rows.map(r => r.baseline), true);
48
+ const available = Math.min(...capture.rows.map(r => r.jev.length));
49
+ const passCounts = [...new Set([...BENCHMARK_PASS_COUNTS.filter(n => n <= available), available])];
50
+ const lines = [
51
+ `Comparison across ${capture.rows.length} labeled decisions (tuning + holdout):`, '',
52
+ '| Configuration | Input tokens | Cached input (subset) | Output tokens | Total tokens | Mean ms/decision | Estimated USD |',
53
+ '|---|---:|---:|---:|---:|---:|---:|',
54
+ ...passCounts.map(passes => {
55
+ const samples = capture.rows.flatMap(r => r.jev.slice(0, passes));
56
+ const t = totals(samples, false);
57
+ return `| Jev, ${passes} pass${passes === 1 ? '' : 'es'} | ${display(t.input)} | n/a | ${display(t.output)} | ${display(t.input === null || t.output === null ? null : t.input + t.output)} | ${t.latency.toFixed(1)} | ${money(t.cost)} |`;
58
+ }),
59
+ `| ${capture.baselineLabel.replace(/[|\r\n]/g, ' ')} | ${display(baseline.input)} | ${display(baseline.cached)} | ${display(baseline.output)} | ${display(baseline.input === null || baseline.output === null ? null : baseline.input + baseline.output)} | ${baseline.latency.toFixed(1)} | ${money(baseline.cost)} |`, '',
60
+ capture.rows.some(r => r.baseline.cost || r.jev.some(s => s.cost)) ? 'Captured costs use provider-reported USD or sourced rates recorded per observation; unknown costs remain unknown.' : pricing ? `Rates: decision.ask.cost.* (${pricing.source ?? 'manual'}, checked ${pricing.checked_at ?? 'unknown'}).` : 'Costs unknown: no explicit decision.ask.cost.* rates.',
61
+ 'Costs use reported usage and explicit pricing assumptions, not subscription invoices.',
62
+ 'Standalone baseline requests do not measure the marginal cost of an inline LLM decision.',
63
+ ];
64
+ return lines.join('\n');
65
+ }
66
+ export function benchmarkDigest(cases) {
67
+ return createHash('sha256').update(JSON.stringify(cases)).digest('hex');
68
+ }
69
+ export function validateBenchmarkCases(value) {
70
+ if (!Array.isArray(value) || value.length < 2 || value.length > 100)
71
+ throw new Error('Benchmark needs 2..100 labeled cases.');
72
+ const ids = new Set();
73
+ for (const c of value) {
74
+ if (!fields(c, ['id', 'split', 'input', 'expected']) || typeof c.id !== 'string' || !/^[A-Za-z0-9_-]{1,100}$/.test(c.id) || ids.has(c.id) || !['tuning', 'holdout'].includes(c.split)
75
+ || !fields(c.input, ['question', 'mode', 'context']) || (c.input.context !== undefined && (typeof c.input.context !== 'string' || c.input.context.length > 100_000)) || typeof c.input.question !== 'string' || !c.input.question.trim() || c.input.question.length > 10_000
76
+ || !c.input.mode || !['yes-no', 'choices', 'scale'].includes(c.input.mode.kind))
77
+ throw new Error('Invalid or duplicate benchmark case.');
78
+ const mode = c.input.mode;
79
+ if (!fields(mode, mode.kind === 'choices' ? ['kind', 'choices'] : mode.kind === 'scale' ? ['kind', 'low', 'high'] : ['kind']))
80
+ throw new Error('Invalid decision mode fields.');
81
+ if (mode.kind === 'yes-no' ? typeof c.expected !== 'boolean'
82
+ : mode.kind === 'choices' ? !Array.isArray(mode.choices) || mode.choices.length < 2 || mode.choices.length > 100 || mode.choices.some(v => typeof v !== 'string' || !v.trim() || v.length > 1000) || new Set(mode.choices).size !== mode.choices.length || !mode.choices.includes(c.expected)
83
+ : !Number.isSafeInteger(mode.low) || !Number.isSafeInteger(mode.high) || mode.low >= mode.high || typeof c.expected !== 'number' || !Number.isInteger(c.expected) || c.expected < mode.low || c.expected > mode.high)
84
+ throw new Error('Gold answer does not match decision mode.');
85
+ ids.add(c.id);
86
+ }
87
+ if (!value.some(c => c.split === 'tuning') || !value.some(c => c.split === 'holdout'))
88
+ throw new Error('Both tuning and holdout cases are required.');
89
+ }
90
+ function validObservation(r) {
91
+ return !!r && ['answered', 'fallback'].includes(r.status) && (r.answer === null || ['string', 'number', 'boolean'].includes(typeof r.answer))
92
+ && (typeof r.answer !== 'number' || Number.isFinite(r.answer))
93
+ && (r.confidence === null || Number.isFinite(r.confidence) && r.confidence >= 0 && r.confidence <= 1)
94
+ && (r.probability === undefined || Number.isFinite(r.probability) && r.probability >= 0 && r.probability <= 1)
95
+ && Number.isFinite(r.latencyMs) && r.latencyMs >= 0
96
+ && (r.model === null || typeof r.model === 'string')
97
+ && (r.usage === null || [r.usage.inputTokens, r.usage.outputTokens].every(n => n === null || Number.isSafeInteger(n) && n >= 0))
98
+ && (r.usage?.cachedInputTokens == null || Number.isSafeInteger(r.usage.cachedInputTokens) && r.usage.cachedInputTokens >= 0
99
+ && r.usage.inputTokens !== null && r.usage.cachedInputTokens <= r.usage.inputTokens)
100
+ && (r.status !== 'answered' || r.answer !== null);
101
+ }
102
+ function modeAnswer(c, r) {
103
+ if (r.status !== 'answered')
104
+ return true;
105
+ const m = c.input.mode;
106
+ return m.kind === 'yes-no' ? typeof r.answer === 'boolean' : m.kind === 'choices' ? m.choices.includes(r.answer)
107
+ : typeof r.answer === 'number' && Number.isInteger(r.answer) && r.answer >= m.low && r.answer <= m.high;
108
+ }
109
+ function validPrice(price) {
110
+ return fields(price, ['model', 'source', 'checkedAt', 'inputPerMillionUsd', 'outputPerMillionUsd', 'cachedInputPerMillionUsd'])
111
+ && validTokenPrice(price) && typeof price.model === 'string' && !!price.model.trim()
112
+ && typeof price.source === 'string' && /^https:\/\//.test(price.source)
113
+ && typeof price.checkedAt === 'string' && Number.isFinite(Date.parse(price.checkedAt));
114
+ }
115
+ function validCost(sample) {
116
+ if (sample.cost === undefined)
117
+ return true;
118
+ const cost = sample.cost;
119
+ if (cost?.source === 'provider-reported')
120
+ return fields(cost, ['usd', 'price', 'source'])
121
+ && cost.price === null && typeof cost.usd === 'number' && Number.isFinite(cost.usd) && cost.usd >= 0;
122
+ return fields(cost, ['usd', 'price']) && (cost.price === null ? cost.usd === null
123
+ : validPrice(cost.price) && cost.price.model === sample.model && cost.usd === tokenCost(sample.usage, cost.price));
124
+ }
125
+ /** Opt-in only. One invocation is one provider call; production ask never calls this collector. */
126
+ export async function collectBenchmark(dataset, options) {
127
+ validateBenchmarkCases(dataset);
128
+ if (options.live !== true || !Number.isSafeInteger(options.maxCalls) || options.maxCalls < 0)
129
+ throw new Error('Explicit live opt-in and nonnegative maxCalls required.');
130
+ if (!Number.isFinite(options.maxUsd) || options.maxUsd <= 0)
131
+ throw new Error('Positive maxUsd required for live collection.');
132
+ if (!Number.isInteger(options.passes) || options.passes < 1 || options.passes > MAX_BENCHMARK_PASSES)
133
+ throw new Error('passes must be 1..20.');
134
+ if (options.jevCapture && options.extendCapture)
135
+ throw new Error('Cannot combine Jev reuse and capture extension.');
136
+ const cases = splitBenchmarkHoldout(dataset, options.holdout ?? 0.3, options.seed ?? '3291');
137
+ const prior = options.extendCapture;
138
+ if (prior)
139
+ validateBenchmarkCapture(cases, prior);
140
+ const reused = options.jevCapture;
141
+ if (reused) {
142
+ validateBenchmarkCapture(cases, reused);
143
+ if (reused.rows.length !== cases.length || reused.rows.some(row => row.jev.length !== options.passes))
144
+ throw new Error('Reused Jev capture must contain every case with the same pass count.');
145
+ }
146
+ if (typeof options.baselineLabel !== 'string' || !options.baselineLabel.trim() || options.baselineLabel.length > 200
147
+ || prior && prior.baselineLabel !== options.baselineLabel)
148
+ throw new Error('Invalid or changed baseline label.');
149
+ const jevPrices = options.jevPrices ?? Object.entries(BUNDLED_JEV_PRICING.models).map(([model, price]) => ({
150
+ ...price, model, source: BUNDLED_JEV_PRICING.source, checkedAt: BUNDLED_JEV_PRICING.checkedAt,
151
+ }));
152
+ for (const prices of [jevPrices, options.baselinePrices ?? []]) {
153
+ if (prices.some(p => !validPrice(p)) || new Set(prices.map(p => p.model)).size !== prices.length)
154
+ throw new Error('Invalid or duplicate sourced model rates.');
155
+ }
156
+ const rows = structuredClone(prior?.rows ?? []);
157
+ if (rows.some(row => row.jev.length > options.passes))
158
+ throw new Error('Extension cannot remove existing passes.');
159
+ const planned = cases.reduce((sum, c) => {
160
+ const row = rows.find(r => r.id === c.id);
161
+ return sum + (row ? 0 : 1) + (reused ? 0 : Math.max(0, options.passes - (row?.jev.length ?? 0)));
162
+ }, 0);
163
+ if (planned > options.maxCalls)
164
+ throw new Error(`Insufficient maxCalls: need ${planned}; no providers invoked.`);
165
+ const now = options.now ?? Date.now;
166
+ let calls = 0, knownUsd = 0;
167
+ for (const row of rows)
168
+ for (const [index, sample] of [row.baseline, ...row.jev].entries()) {
169
+ // Legacy captures can acquire a sourced cost without changing their recorded response.
170
+ if (!sample.cost) {
171
+ const price = (index === 0 ? options.baselinePrices ?? [] : jevPrices).find(p => p.model === sample.model) ?? null;
172
+ sample.cost = { usd: price ? tokenCost(sample.usage, price) : null, price: structuredClone(price) };
173
+ }
174
+ if (sample.cost.usd === null)
175
+ throw new Error('Cannot safely extend an unpriced capture.');
176
+ knownUsd += sample.cost.usd;
177
+ }
178
+ let stopReason = 'incomplete';
179
+ const snapshot = () => ({ schema: 'aiwg-decision-benchmark-capture/v1',
180
+ datasetDigest: benchmarkDigest(cases), capturedAt: new Date(now()).toISOString(),
181
+ baselineLabel: options.baselineLabel, rows: structuredClone(rows), collection: { stopReason, calls, knownUsd } });
182
+ if (planned === 0 || knownUsd >= options.maxUsd * 0.8) {
183
+ stopReason = planned === 0 ? 'complete' : 'usd-limit';
184
+ const capture = snapshot();
185
+ validateBenchmarkCapture(cases, capture);
186
+ if (options.onCheckpoint)
187
+ await options.onCheckpoint(capture);
188
+ return capture;
189
+ }
190
+ const environment = options.env ?? process.env;
191
+ if (!reused && !options.jev && planned > 0 && cases.some(c => (rows.find(r => r.id === c.id)?.jev.length ?? 0) < options.passes)
192
+ && !(environment.JEV_API_KEY?.trim() || environment.AIWG_DECISION_JEV_API_KEY?.trim()))
193
+ throw new Error('Live benchmark requires JEV_API_KEY or AIWG_DECISION_JEV_API_KEY in the caller environment.');
194
+ const jev = options.jev ?? (async (input) => {
195
+ // Force environment-only lookup; lookupJevCredential skips credential files with this explicit flag.
196
+ return askDecision(input, { env: { ...environment, AIWG_DECISION_ENABLED: '1' }, now });
197
+ });
198
+ const invoke = async (c, baseline) => {
199
+ const started = now();
200
+ calls++;
201
+ let result;
202
+ try {
203
+ result = await (baseline ? options.baseline : jev)({ ...structuredClone(c.input), threshold: 0, passes: 1, escalationPasses: 1 });
204
+ }
205
+ catch {
206
+ // No retry: a failed request can still be billable. Stop when its usage is unknown.
207
+ result = { status: 'fallback', answer: null, confidence: null, model: null, usage: null, latencyMs: 0 };
208
+ }
209
+ const sample = { answer: result.answer, confidence: result.confidence, status: result.status,
210
+ model: result.model, usage: structuredClone(result.usage), latencyMs: Math.max(0, now() - started),
211
+ ...(result.probability === undefined ? {} : { probability: result.probability }) };
212
+ if (!validObservation(sample) || !modeAnswer(c, sample))
213
+ throw new Error(`Invalid provider observation: ${c.id}`);
214
+ const price = (baseline ? options.baselinePrices ?? [] : jevPrices).find(p => p.model === sample.model) ?? null;
215
+ sample.cost = result.cost?.source === 'provider-reported' && validCost(result)
216
+ ? structuredClone(result.cost) : { usd: price ? tokenCost(sample.usage, price) : null, price: structuredClone(price) };
217
+ const usd = sample.cost.usd;
218
+ if (usd === null)
219
+ stopReason = 'unknown-cost';
220
+ else {
221
+ knownUsd += usd;
222
+ if (knownUsd >= options.maxUsd * 0.8)
223
+ stopReason = 'usd-limit';
224
+ }
225
+ return sample;
226
+ };
227
+ for (const c of cases) {
228
+ let row = rows.find(r => r.id === c.id);
229
+ if (!row) {
230
+ row = { id: c.id, baseline: await invoke(c, true),
231
+ jev: structuredClone(reused?.rows.find(r => r.id === c.id).jev ?? []) };
232
+ rows.push(row);
233
+ }
234
+ if (options.onCheckpoint)
235
+ await options.onCheckpoint(snapshot());
236
+ if (stopReason !== 'incomplete')
237
+ break;
238
+ while (row.jev.length < options.passes) {
239
+ row.jev.push(await invoke(c, false));
240
+ if (options.onCheckpoint)
241
+ await options.onCheckpoint(snapshot());
242
+ if (stopReason !== 'incomplete')
243
+ break;
244
+ }
245
+ if (stopReason !== 'incomplete')
246
+ break;
247
+ }
248
+ if (cases.every(c => rows.some(r => r.id === c.id && r.jev.length === options.passes)))
249
+ stopReason = 'complete';
250
+ const capture = snapshot();
251
+ validateBenchmarkCapture(cases, capture);
252
+ if (options.onCheckpoint)
253
+ await options.onCheckpoint(capture);
254
+ return capture;
255
+ }
256
+ function mean(values) {
257
+ return values.length ? values.reduce((sum, n) => sum + n, 0) / values.length : null;
258
+ }
259
+ /** Sample SD: one observation cannot estimate variation and is reported as null. */
260
+ function sampleStdDev(values) {
261
+ if (values.length < 2)
262
+ return null;
263
+ const average = mean(values);
264
+ return Math.sqrt(values.reduce((sum, n) => sum + (n - average) ** 2, 0) / (values.length - 1));
265
+ }
266
+ export function benchmarkScoringDeviation(cases, capture, passes) {
267
+ validateBenchmarkCapture(cases, capture);
268
+ if (!Number.isInteger(passes) || passes < 1 || passes > MAX_BENCHMARK_PASSES)
269
+ throw new Error('Invalid deviation pass count.');
270
+ const rows = cases.map(c => {
271
+ const row = capture.rows.find(r => r.id === c.id);
272
+ if (!row || row.jev.length < passes)
273
+ throw new Error('Insufficient observations for deviation.');
274
+ const samples = row.jev.slice(0, passes);
275
+ const answered = samples.filter(r => r.status === 'answered' && r.answer !== null);
276
+ const counts = new Map();
277
+ for (const r of answered) {
278
+ const key = JSON.stringify(r.answer);
279
+ counts.set(key, (counts.get(key) ?? 0) + 1);
280
+ }
281
+ const confidence = samples.flatMap(r => r.confidence === null ? [] : [r.confidence]);
282
+ const scores = c.input.mode.kind === 'scale' ? answered.flatMap(r => typeof r.answer === 'number' ? [r.answer] : []) : [];
283
+ const probabilities = c.input.mode.kind === 'yes-no' ? samples.flatMap(r => r.probability === undefined ? [] : [r.probability]) : [];
284
+ return { id: c.id, split: c.split, kind: c.input.mode.kind, observations: samples.length, answered: answered.length,
285
+ rawCorrect: answered.filter(r => r.answer === c.expected).length,
286
+ nonModalAnswers: answered.length - Math.max(0, ...counts.values()),
287
+ confidenceMean: mean(confidence), confidenceStdDev: sampleStdDev(confidence),
288
+ scaleScoreMean: mean(scores), scaleScoreStdDev: sampleStdDev(scores),
289
+ scaleScoreMin: scores.length ? Math.min(...scores) : null, scaleScoreMax: scores.length ? Math.max(...scores) : null,
290
+ scaleMaeGold: mean(scores.map(n => Math.abs(n - Number(c.expected)))),
291
+ scaleMaeBaseline: row.baseline.status === 'answered' && typeof row.baseline.answer === 'number'
292
+ ? mean(scores.map(n => Math.abs(n - Number(row.baseline.answer)))) : null,
293
+ yesNoBrier: mean(probabilities.map(p => (p - Number(c.expected)) ** 2)),
294
+ };
295
+ });
296
+ const present = (values) => values.filter((n) => n !== null);
297
+ const answered = rows.reduce((sum, r) => sum + r.answered, 0);
298
+ return { passes, observations: cases.length * passes, answered,
299
+ rawAccuracy: rows.reduce((sum, r) => sum + r.rawCorrect, 0) / (cases.length * passes),
300
+ answerDisagreementRate: answered ? rows.reduce((sum, r) => sum + r.nonModalAnswers, 0) / answered : null,
301
+ meanConfidence: mean(present(rows.map(r => r.confidenceMean))),
302
+ meanWithinCaseConfidenceStdDev: mean(present(rows.map(r => r.confidenceStdDev))),
303
+ meanWithinCaseScaleScoreStdDev: mean(present(rows.map(r => r.scaleScoreStdDev))),
304
+ meanScaleMaeGold: mean(present(rows.map(r => r.scaleMaeGold))),
305
+ meanScaleMaeBaseline: mean(present(rows.map(r => r.scaleMaeBaseline))),
306
+ meanYesNoBrier: mean(present(rows.map(r => r.yesNoBrier))), cases: rows };
307
+ }
308
+ function select(observations, policy) {
309
+ const samples = observations.slice(0, policy.passes);
310
+ const votes = new Map();
311
+ for (const r of samples) {
312
+ if (r.status !== 'answered' || r.answer === null || r.confidence === null)
313
+ continue;
314
+ const key = JSON.stringify(r.answer);
315
+ const vote = votes.get(key) ?? { answer: r.answer, count: 0, confidence: 0 };
316
+ vote.count++;
317
+ vote.confidence += r.confidence;
318
+ votes.set(key, vote);
319
+ }
320
+ const ranked = [...votes.values()].sort((a, b) => b.count - a.count);
321
+ const best = ranked[0];
322
+ if (!best || ranked[1]?.count === best.count || best.count / policy.passes < policy.agreement
323
+ || best.confidence / best.count < policy.threshold)
324
+ return null;
325
+ return best.answer;
326
+ }
327
+ /** Threshold/pass/consensus sweeps reuse captures. Holdout NEVER selects the policy. */
328
+ export function analyzeBenchmark(cases, capture, policies, pricing, options = {}) {
329
+ validateBenchmarkCases(cases);
330
+ validateBenchmarkCapture(cases, capture);
331
+ validateRates(pricing);
332
+ if (capture.rows.length !== cases.length)
333
+ throw new Error('Incomplete capture: extend before analysis.');
334
+ if (!policies.length || policies.length > 100 || policies.some(p => !Number.isInteger(p.passes) || p.passes < 1 || p.passes > MAX_BENCHMARK_PASSES
335
+ || !Number.isFinite(p.threshold) || p.threshold < 0 || p.threshold > 1 || !Number.isFinite(p.agreement) || p.agreement <= 0 || p.agreement > 1))
336
+ throw new Error('Invalid benchmark policy grid.');
337
+ const minimumAccuracy = options.minimumAccuracy ?? 1;
338
+ const maximumFallbackRate = options.maximumFallbackRate ?? 1;
339
+ if (![minimumAccuracy, maximumFallbackRate].every(n => Number.isFinite(n) && n >= 0 && n <= 1))
340
+ throw new Error('Invalid benchmark quality limits.');
341
+ const byId = new Map(capture.rows.map(r => [r.id, r]));
342
+ for (const c of cases) {
343
+ const row = byId.get(c.id);
344
+ if (!row || !validObservation(row.baseline) || !modeAnswer(c, row.baseline) || !Array.isArray(row.jev)
345
+ || row.jev.length < Math.max(...policies.map(p => p.passes)) || row.jev.some(r => !validObservation(r) || !modeAnswer(c, r)))
346
+ throw new Error('Invalid or insufficient captured observations.');
347
+ }
348
+ const measurements = policies.map(policy => {
349
+ const splits = ['tuning', 'holdout'].map(split => {
350
+ const selected = cases.filter(c => c.split === split);
351
+ let accepted = 0, jevCorrect = 0, baselineCorrect = 0, routedCorrect = 0, agreements = 0;
352
+ let jevUsd = 0, baselineUsd = 0, routedUsd = 0, unpriced = 0, unpricedJev = 0, unpricedBaseline = 0, jevMs = 0, baselineMs = 0, routedMs = 0;
353
+ for (const c of selected) {
354
+ const row = byId.get(c.id);
355
+ const answer = select(row.jev, policy);
356
+ const baselineCost = observationCost(row.baseline, pricing, true);
357
+ let caseJevUsd = 0, caseJevMs = 0;
358
+ let unknownJevCost = false;
359
+ for (const r of row.jev.slice(0, policy.passes)) {
360
+ const cost = observationCost(r, pricing);
361
+ if (cost === null)
362
+ unknownJevCost = true;
363
+ else
364
+ caseJevUsd += cost;
365
+ caseJevMs += r.latencyMs;
366
+ }
367
+ if (unknownJevCost)
368
+ unpricedJev++;
369
+ if (baselineCost === null)
370
+ unpricedBaseline++;
371
+ if (unknownJevCost || baselineCost === null)
372
+ unpriced++;
373
+ jevUsd += caseJevUsd;
374
+ baselineUsd += baselineCost ?? 0;
375
+ routedUsd += caseJevUsd + (answer === null ? baselineCost ?? 0 : 0);
376
+ jevMs += caseJevMs;
377
+ baselineMs += row.baseline.latencyMs;
378
+ routedMs += caseJevMs + (answer === null ? row.baseline.latencyMs : 0);
379
+ if (row.baseline.status === 'answered' && row.baseline.answer === c.expected)
380
+ baselineCorrect++;
381
+ if (answer !== null) {
382
+ accepted++;
383
+ if (answer === c.expected)
384
+ jevCorrect++;
385
+ if (row.baseline.status === 'answered' && answer === row.baseline.answer)
386
+ agreements++;
387
+ }
388
+ if ((answer ?? (row.baseline.status === 'answered' ? row.baseline.answer : null)) === c.expected)
389
+ routedCorrect++;
390
+ }
391
+ return { split, cases: selected.length, accepted, fallbackRate: 1 - accepted / selected.length,
392
+ selectiveAccuracy: accepted ? jevCorrect / accepted : null,
393
+ baselineAccuracy: baselineCorrect / selected.length, routedAccuracy: routedCorrect / selected.length,
394
+ agreementWithBaseline: accepted ? agreements / accepted : null,
395
+ estimatedJevUsd: unpricedJev ? null : jevUsd, baselineUsd: unpricedBaseline ? null : baselineUsd,
396
+ routedUsd: unpriced ? null : routedUsd, savingsUsd: unpriced ? null : baselineUsd - routedUsd,
397
+ unpricedCases: unpriced, meanJevMs: jevMs / selected.length, meanBaselineMs: baselineMs / selected.length,
398
+ meanRoutedMs: routedMs / selected.length };
399
+ });
400
+ return { policy, tuning: splits[0], holdout: splits[1] };
401
+ });
402
+ const eligible = measurements.filter(m => m.tuning.routedAccuracy >= minimumAccuracy && m.tuning.fallbackRate <= maximumFallbackRate);
403
+ // With no comparable USD rate, use measured routed latency rather than inventing a price.
404
+ const useCost = eligible.every(m => m.tuning.routedUsd !== null);
405
+ eligible.sort((a, b) => (useCost ? a.tuning.routedUsd - b.tuning.routedUsd : a.tuning.meanRoutedMs - b.tuning.meanRoutedMs)
406
+ || b.policy.threshold - a.policy.threshold);
407
+ return { schema: 'aiwg-decision-benchmark-report/v1', datasetDigest: capture.datasetDigest,
408
+ baselineLabel: capture.baselineLabel, pricing: pricing ?? null,
409
+ minimumAccuracy, maximumFallbackRate, recommended: eligible[0]?.policy ?? null, measurements,
410
+ scoringDeviation: [...new Set(policies.map(p => p.passes))].sort((a, b) => a - b).map(passes => benchmarkScoringDeviation(cases, capture, passes)),
411
+ deviationDefinitions: {
412
+ scaleScoreStdDev: 'Within-case sample SD of raw 1-N scale scores, averaged across scale cases; null for one pass.',
413
+ confidenceStdDev: 'Within-case sample SD of reported confidence (0..1), averaged across cases; not a calibration guarantee.',
414
+ answerDisagreementRate: 'Fraction of answered passes differing from their case modal answer; categorical labels are never assigned numeric scores.',
415
+ scaleMae: 'Mean absolute difference of raw scale scores against gold labels or the saved LLM score; expressed in scale points.',
416
+ yesNoBrier: 'Mean squared error of reported truth probability against gold boolean labels; null when probability is unavailable.',
417
+ },
418
+ limitations: ['Small labeled benchmark; LLM agreement is not ground truth.',
419
+ 'Recommendations use tuning cases only; evaluate holdout before adoption. No configuration is changed.',
420
+ 'Serial pass latency; modeled routing cost includes LLM cost on fallbacks. Benchmark collection itself pays both arms.',
421
+ 'Repeated Jev passes may be correlated; identical answers are not independent confidence evidence.',
422
+ 'Standalone baseline requests do not estimate marginal reasoning inside an existing conversation.',
423
+ 'Legacy captures may lack cached-input counts; cache-aware prices then leave costs unknown. No subscription invoice is inferred.',
424
+ 'Prices are API-equivalent estimates, not subscription invoices. Missing usage or rates makes savings unknown.'] };
425
+ }
426
+ export function benchmarkDeviationComparison(report) {
427
+ const format = (n, multiplier = 1) => n === null ? 'n/a' : (n * multiplier).toFixed(4);
428
+ return [
429
+ 'Scoring deviation across repeated passes:', '',
430
+ '| Passes | Raw accuracy % | Mean confidence % | Confidence SD (pp) | Scale-score SD (points) | Scale MAE vs gold | Answer disagreement % | Yes/no Brier |',
431
+ '|---|---:|---:|---:|---:|---:|---:|---:|',
432
+ ...report.scoringDeviation.map(d => `| ${d.passes} | ${format(d.rawAccuracy, 100)} | ${format(d.meanConfidence, 100)} | ${format(d.meanWithinCaseConfidenceStdDev, 100)} | ${format(d.meanWithinCaseScaleScoreStdDev)} | ${format(d.meanScaleMaeGold)} | ${format(d.answerDisagreementRate, 100)} | ${format(d.meanYesNoBrier)} |`), '',
433
+ 'SD is the within-case sample standard deviation, averaged across applicable cases. One pass cannot estimate SD (n/a).',
434
+ 'Scale scores use their original rubric points; categorical answers have no numeric score. Raw accuracy counts every pass, before confidence-based routing.',
435
+ 'Pass-count rows are nested prefixes of one capture, not independent experiments; repeated calls may be correlated.',
436
+ ].join('\n');
437
+ }
438
+ export const MAX_BENCHMARK_BYTES = 4 * 1024 * 1024;
439
+ function fields(value, allowed) {
440
+ return !!value && typeof value === 'object' && !Array.isArray(value)
441
+ && Object.keys(value).every(key => allowed.includes(key));
442
+ }
443
+ /** Captures contain values only: no paths, executable hooks, or provider settings. */
444
+ export function validateBenchmarkCapture(cases, value) {
445
+ validateBenchmarkCases(cases);
446
+ const c = value;
447
+ if (!fields(c, ['schema', 'datasetDigest', 'capturedAt', 'baselineLabel', 'rows', 'collection'])
448
+ || c.schema !== 'aiwg-decision-benchmark-capture/v1' || c.datasetDigest !== benchmarkDigest(cases)
449
+ || typeof c.capturedAt !== 'string' || !/^\d{4}-\d\d-\d\dT/.test(c.capturedAt) || !Number.isFinite(Date.parse(c.capturedAt))
450
+ || typeof c.baselineLabel !== 'string' || !c.baselineLabel.trim() || c.baselineLabel.length > 200
451
+ || !Array.isArray(c.rows) || c.rows.length > cases.length
452
+ || ((!c.collection || c.collection.stopReason === 'complete') && c.rows.length !== cases.length)
453
+ || new Set(c.rows.map(r => r?.id)).size !== c.rows.length)
454
+ throw new Error('Capture does not match this dataset or schema.');
455
+ if (c.collection !== undefined && (!fields(c.collection, ['stopReason', 'calls', 'knownUsd'])
456
+ || !['complete', 'incomplete', 'usd-limit', 'unknown-cost'].includes(c.collection.stopReason)
457
+ || !Number.isSafeInteger(c.collection.calls) || c.collection.calls < 0
458
+ || !Number.isFinite(c.collection.knownUsd) || c.collection.knownUsd < 0))
459
+ throw new Error('Invalid collection totals.');
460
+ const observation = (r) => fields(r, ['answer', 'confidence', 'probability', 'status', 'model', 'usage', 'latencyMs', 'cost'])
461
+ && (r.usage === null || fields(r.usage, ['inputTokens', 'outputTokens', 'cachedInputTokens'])) && validObservation(r) && validCost(r);
462
+ for (const row of c.rows) {
463
+ const item = cases.find(item => item.id === row?.id);
464
+ if (!fields(row, ['id', 'baseline', 'jev']) || !item || !observation(row.baseline) || !modeAnswer(item, row.baseline)
465
+ || !Array.isArray(row.jev) || row.jev.length < (c.collection && c.collection.stopReason !== 'complete' ? 0 : 1) || row.jev.length > MAX_BENCHMARK_PASSES
466
+ || row.jev.some(r => !observation(r) || !modeAnswer(item, r)))
467
+ throw new Error('Invalid captured observations.');
468
+ }
469
+ if (Buffer.byteLength(JSON.stringify(c)) > MAX_BENCHMARK_BYTES)
470
+ throw new Error('Capture exceeds size limit.');
471
+ }
472
+ /** Stable by ID and seed, independent of file order and observed answers. */
473
+ export function splitBenchmarkHoldout(cases, fraction, seed = '3291') {
474
+ validateBenchmarkCases(cases);
475
+ if (!Number.isFinite(fraction) || fraction <= 0 || fraction >= 1)
476
+ throw new Error('Holdout fraction must be between 0 and 1.');
477
+ const count = Math.max(1, Math.min(cases.length - 1, Math.round(cases.length * fraction)));
478
+ const hash = (id) => createHash('sha256').update(JSON.stringify([seed, id])).digest('hex');
479
+ const ids = new Set([...cases].sort((a, b) => hash(a.id).localeCompare(hash(b.id))).slice(0, count).map(c => c.id));
480
+ return cases.map(c => ({ ...c, split: ids.has(c.id) ? 'holdout' : 'tuning' }));
481
+ }
482
+ //# sourceMappingURL=benchmark.js.map
@@ -62,6 +62,7 @@ import { projectAiwgPath, projectAiwgWritePath } from '../config/project-artifac
62
62
  import { redactText } from '../governance/redaction.js';
63
63
  import { CanonicalJsonByteEstimator } from './context-plan.js';
64
64
  import { resolveActiveProvider } from '../cli/provider-resolution.js';
65
+ import { validateDecisionConfig } from '../config/decision-config.js';
65
66
  import { readAiwgConfig } from '../config/aiwg-config.js';
66
67
  import { decisionSavingsWeights, estimateDecisionSavings } from './savings.js';
67
68
  export const DECISION_LEDGER_SCHEMA = 'aiwg-decision-ledger-record/v2';
@@ -231,7 +232,7 @@ export function buildDecisionLedgerRecord(input, result, context) {
231
232
  const timestamp = new Date((context.now ?? Date.now)()).toISOString();
232
233
  const legacy = estimateSavingsV1(input, result);
233
234
  const inputTokens = new CanonicalJsonByteEstimator().estimate(contextMessageForEstimate(input)).tokens;
234
- const savings = estimateDecisionSavings(inputTokens, result.status === 'answered', (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0), context.savingsWeights ?? decisionSavingsWeights());
235
+ const savings = estimateDecisionSavings(inputTokens, result.status === 'answered', (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0), context.savingsWeights ?? decisionSavingsWeights(), result.usage);
235
236
  const caller = context.caller && ATTRIBUTION_FIELD_PATTERN.test(context.caller) ? context.caller : null;
236
237
  const rawAnswer = result.answer;
237
238
  const answer = typeof rawAnswer === 'string' ? sanitizeField(rawAnswer) : rawAnswer;
@@ -669,6 +670,11 @@ function isDecisionLedgerRecord(value) {
669
670
  if (!isFiniteNumber(method[key]) || method[key] < 0 || method[key] > 1_000_000)
670
671
  return false;
671
672
  }
673
+ if (validateDecisionConfig({ ask: { cost: Object.fromEntries(['llm_per_mtok', 'jev_per_mtok', 'jev_input_per_mtok', 'jev_output_per_mtok', 'source', 'checked_at', 'jev_model']
674
+ .filter(key => method[key] !== undefined).map(key => [key, method[key]])) } }).length)
675
+ return false;
676
+ if (method.jev_rate_basis !== undefined && !['split', 'blended'].includes(method.jev_rate_basis))
677
+ return false;
672
678
  if (typeof record.sessionSource !== 'string')
673
679
  return false;
674
680
  for (const key of ['llm_tokens_avoided', 'jev_tokens']) {
@@ -787,7 +793,7 @@ function groupKeyFor(record, groupBy) {
787
793
  }
788
794
  function newAccumulator() {
789
795
  return {
790
- llmTokensAvoided: 0, costWeightedNetUsd: 0, legacyReestimatedCalls: 0,
796
+ llmTokensAvoided: 0, costWeightedNetUsd: 0, legacyReestimatedCalls: 0, ratesUsed: [],
791
797
  totalCalls: 0, answered: 0, fallback: 0, fallbackCountByReason: {},
792
798
  agreementCount: 0, agreementSum: 0, escalatedCalls: 0, passesRun: 0,
793
799
  confidenceCount: 0, confidenceSum: 0, confidenceMin: null, confidenceMax: null,
@@ -806,6 +812,18 @@ function accumulate(acc, record, weights) {
806
812
  acc.passesRun += record.passesRun ?? (record.fallbackReason === 'not-configured' ? 0 : 1);
807
813
  const inputTokens = record.usage?.inputTokens ?? 0;
808
814
  const outputTokens = record.usage?.outputTokens ?? 0;
815
+ const method = record.schema === DECISION_LEDGER_SCHEMA
816
+ ? record.estimationMethod
817
+ : estimateDecisionSavings(0, false, inputTokens + outputTokens, weights, record.usage).estimationMethod;
818
+ // Input estimates vary per call; group only the recorded rates and provenance.
819
+ const { input_tokens: _input, ...rateMethod } = method;
820
+ const canonicalRates = (rates) => JSON.stringify(Object.entries(rates).sort(([a], [b]) => a.localeCompare(b)));
821
+ const rateKey = canonicalRates(rateMethod);
822
+ const prior = acc.ratesUsed.find(item => canonicalRates(item.method) === rateKey);
823
+ if (prior)
824
+ prior.calls++;
825
+ else
826
+ acc.ratesUsed.push({ calls: 1, method: rateMethod });
809
827
  if (record.schema === DECISION_LEDGER_SCHEMA) {
810
828
  acc.llmTokensAvoided += record.llm_tokens_avoided;
811
829
  acc.costWeightedNetUsd += record.cost_weighted_net_usd;
@@ -815,7 +833,7 @@ function accumulate(acc, record, weights) {
815
833
  // per-kind allowance. Digests alone cannot reconstruct caller text.
816
834
  const recoveredInput = record.status === 'answered'
817
835
  ? Math.max(0, record.estimatedLlmTokensAvoided - DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS[record.decisionKind]) : 0;
818
- const estimate = estimateDecisionSavings(recoveredInput, record.status === 'answered', inputTokens + outputTokens, weights);
836
+ const estimate = estimateDecisionSavings(recoveredInput, record.status === 'answered', inputTokens + outputTokens, weights, record.usage);
819
837
  acc.llmTokensAvoided += estimate.llm_tokens_avoided;
820
838
  acc.costWeightedNetUsd += estimate.cost_weighted_net_usd;
821
839
  acc.legacyReestimatedCalls += 1;
@@ -851,6 +869,7 @@ function finalizeGroup(key, acc) {
851
869
  jev_tokens: acc.jevInputTokens + acc.jevOutputTokens,
852
870
  cost_weighted_net_usd: acc.costWeightedNetUsd,
853
871
  legacyReestimatedCalls: acc.legacyReestimatedCalls,
872
+ ratesUsed: acc.ratesUsed,
854
873
  totalCalls: acc.totalCalls,
855
874
  answered: acc.answered,
856
875
  fallback: acc.fallback,