akm-cli 0.6.0 → 0.7.0-rc1

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 (319) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +672 -29
  3. package/dist/{commands → src/commands}/config-cli.js +5 -4
  4. package/dist/src/commands/distill.js +283 -0
  5. package/dist/src/commands/events.js +108 -0
  6. package/dist/src/commands/history.js +120 -0
  7. package/dist/{commands → src/commands}/installed-stashes.js +28 -2
  8. package/dist/src/commands/proposal.js +119 -0
  9. package/dist/src/commands/propose.js +171 -0
  10. package/dist/src/commands/reflect.js +193 -0
  11. package/dist/{commands → src/commands}/registry-search.js +2 -1
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +74 -1
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +67 -2
  16. package/dist/{core → src/core}/asset-ref.js +5 -5
  17. package/dist/{core → src/core}/asset-spec.js +12 -0
  18. package/dist/{core → src/core}/common.js +1 -1
  19. package/dist/{core → src/core}/config.js +175 -121
  20. package/dist/{core → src/core}/errors.js +4 -0
  21. package/dist/src/core/events.js +239 -0
  22. package/dist/src/core/lesson-lint.js +86 -0
  23. package/dist/src/core/proposals.js +406 -0
  24. package/dist/src/core/warn.js +72 -0
  25. package/dist/{core → src/core}/write-source.js +80 -5
  26. package/dist/{indexer → src/indexer}/db-search.js +119 -27
  27. package/dist/{indexer → src/indexer}/db.js +76 -23
  28. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  29. package/dist/src/indexer/graph-boost.js +179 -0
  30. package/dist/src/indexer/graph-extraction.js +212 -0
  31. package/dist/{indexer → src/indexer}/indexer.js +73 -6
  32. package/dist/src/indexer/memory-inference.js +263 -0
  33. package/dist/{indexer → src/indexer}/metadata.js +114 -11
  34. package/dist/src/integrations/agent/config.js +292 -0
  35. package/dist/src/integrations/agent/detect.js +94 -0
  36. package/dist/src/integrations/agent/index.js +17 -0
  37. package/dist/src/integrations/agent/profiles.js +65 -0
  38. package/dist/src/integrations/agent/prompts.js +167 -0
  39. package/dist/src/integrations/agent/spawn.js +221 -0
  40. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  41. package/dist/{llm → src/llm}/client.js +33 -2
  42. package/dist/src/llm/feature-gate.js +108 -0
  43. package/dist/src/llm/graph-extract.js +107 -0
  44. package/dist/src/llm/index-passes.js +35 -0
  45. package/dist/src/llm/memory-infer.js +86 -0
  46. package/dist/{output → src/output}/renderers.js +60 -1
  47. package/dist/src/output/shapes.js +516 -0
  48. package/dist/{output → src/output}/text.js +447 -4
  49. package/dist/{registry → src/registry}/build-index.js +14 -4
  50. package/dist/{registry → src/registry}/factory.js +0 -8
  51. package/dist/{registry → src/registry}/providers/static-index.js +3 -2
  52. package/dist/{registry → src/registry}/resolve.js +68 -2
  53. package/dist/{setup → src/setup}/setup.js +43 -5
  54. package/dist/{sources → src/sources}/providers/git.js +7 -15
  55. package/dist/{wiki → src/wiki}/wiki.js +9 -11
  56. package/dist/tests/add-website-source.test.js +119 -0
  57. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  58. package/dist/tests/agent/agent-config.test.js +221 -0
  59. package/dist/tests/agent/agent-detect.test.js +100 -0
  60. package/dist/tests/agent/agent-spawn.test.js +234 -0
  61. package/dist/tests/agent-output.test.js +186 -0
  62. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  63. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  64. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  65. package/dist/tests/asset-ref.test.js +192 -0
  66. package/dist/tests/asset-registry.test.js +103 -0
  67. package/dist/tests/asset-spec.test.js +241 -0
  68. package/dist/tests/bench/attribution.test.js +995 -0
  69. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  70. package/dist/tests/bench/cleanup.js +203 -0
  71. package/dist/tests/bench/cleanup.test.js +166 -0
  72. package/dist/tests/bench/cli.js +683 -0
  73. package/dist/tests/bench/cli.test.js +177 -0
  74. package/dist/tests/bench/compare.test.js +556 -0
  75. package/dist/tests/bench/corpus.js +314 -0
  76. package/dist/tests/bench/corpus.test.js +258 -0
  77. package/dist/tests/bench/driver.js +346 -0
  78. package/dist/tests/bench/driver.test.js +443 -0
  79. package/dist/tests/bench/evolve-metrics.js +179 -0
  80. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  81. package/dist/tests/bench/evolve.js +580 -0
  82. package/dist/tests/bench/evolve.test.js +616 -0
  83. package/dist/tests/bench/failure-modes.test.js +300 -0
  84. package/dist/tests/bench/feedback-integrity.test.js +456 -0
  85. package/dist/tests/bench/leakage.test.js +125 -0
  86. package/dist/tests/bench/learning-curve.test.js +133 -0
  87. package/dist/tests/bench/metrics.js +2319 -0
  88. package/dist/tests/bench/metrics.test.js +1144 -0
  89. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  90. package/dist/tests/bench/report.js +1821 -0
  91. package/dist/tests/bench/report.test.js +989 -0
  92. package/dist/tests/bench/runner.js +536 -0
  93. package/dist/tests/bench/runner.test.js +958 -0
  94. package/dist/tests/bench/search-bridge.test.js +331 -0
  95. package/dist/tests/bench/tmp.js +41 -0
  96. package/dist/tests/bench/trajectory.js +116 -0
  97. package/dist/tests/bench/trajectory.test.js +127 -0
  98. package/dist/tests/bench/verifier.js +109 -0
  99. package/dist/tests/bench/verifier.test.js +118 -0
  100. package/dist/tests/bench/workflow-evaluator.js +557 -0
  101. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  102. package/dist/tests/bench/workflow-spec.js +358 -0
  103. package/dist/tests/bench/workflow-spec.test.js +363 -0
  104. package/dist/tests/bench/workflow-trace.js +438 -0
  105. package/dist/tests/bench/workflow-trace.test.js +254 -0
  106. package/dist/tests/benchmark-search-quality.js +536 -0
  107. package/dist/tests/benchmark-suite.js +1441 -0
  108. package/dist/tests/capture-cli.test.js +112 -0
  109. package/dist/tests/cli-errors.test.js +203 -0
  110. package/dist/tests/commands/events.test.js +370 -0
  111. package/dist/tests/commands/history.test.js +223 -0
  112. package/dist/tests/commands/import.test.js +103 -0
  113. package/dist/tests/commands/proposal-cli.test.js +209 -0
  114. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  115. package/dist/tests/commands/remember.test.js +97 -0
  116. package/dist/tests/commands/scope-flags.test.js +300 -0
  117. package/dist/tests/commands/search.test.js +537 -0
  118. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  119. package/dist/tests/commands/show.test.js +294 -0
  120. package/dist/tests/common.test.js +266 -0
  121. package/dist/tests/completions.test.js +142 -0
  122. package/dist/tests/config-cli.test.js +193 -0
  123. package/dist/tests/config-llm-features.test.js +139 -0
  124. package/dist/tests/config.test.js +544 -0
  125. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  126. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  127. package/dist/tests/contracts/spec-helpers.js +46 -0
  128. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  129. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  130. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  131. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  132. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  133. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  134. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  135. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  136. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  137. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  138. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  139. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  140. package/dist/tests/core/write-source.test.js +366 -0
  141. package/dist/tests/curate-command.test.js +87 -0
  142. package/dist/tests/db-scoring.test.js +201 -0
  143. package/dist/tests/db.test.js +654 -0
  144. package/dist/tests/distill-cli-flag.test.js +208 -0
  145. package/dist/tests/distill.test.js +515 -0
  146. package/dist/tests/docker-install.test.js +120 -0
  147. package/dist/tests/e2e.test.js +1398 -0
  148. package/dist/tests/embedder.test.js +340 -0
  149. package/dist/tests/embedding-model-config.test.js +379 -0
  150. package/dist/tests/feedback-command.test.js +172 -0
  151. package/dist/tests/file-context.test.js +552 -0
  152. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  153. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  154. package/dist/tests/fixtures/stashes/load.js +166 -0
  155. package/dist/tests/fixtures/stashes/load.test.js +88 -0
  156. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  157. package/dist/tests/frontmatter.test.js +190 -0
  158. package/dist/tests/fts-field-weighting.test.js +254 -0
  159. package/dist/tests/fuzzy-search.test.js +230 -0
  160. package/dist/tests/git-provider-clone.test.js +45 -0
  161. package/dist/tests/github.test.js +161 -0
  162. package/dist/tests/graph-boost-ranking.test.js +305 -0
  163. package/dist/tests/graph-extraction.test.js +282 -0
  164. package/dist/tests/helpers/usage-events.js +8 -0
  165. package/dist/tests/index-pass-llm.test.js +161 -0
  166. package/dist/tests/indexer.test.js +559 -0
  167. package/dist/tests/info-command.test.js +166 -0
  168. package/dist/tests/init.test.js +69 -0
  169. package/dist/tests/install-script.test.js +246 -0
  170. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  171. package/dist/tests/issue-36-repro.test.js +304 -0
  172. package/dist/tests/issues-191-194.test.js +160 -0
  173. package/dist/tests/lesson-lint.test.js +111 -0
  174. package/dist/tests/llm-client.test.js +115 -0
  175. package/dist/tests/llm-feature-gate.test.js +151 -0
  176. package/dist/tests/llm.test.js +139 -0
  177. package/dist/tests/lockfile.test.js +216 -0
  178. package/dist/tests/manifest.test.js +205 -0
  179. package/dist/tests/markdown.test.js +126 -0
  180. package/dist/tests/matchers-unit.test.js +189 -0
  181. package/dist/tests/memory-inference.test.js +299 -0
  182. package/dist/tests/merge-scoring.test.js +136 -0
  183. package/dist/tests/metadata.test.js +313 -0
  184. package/dist/tests/migration-help.test.js +89 -0
  185. package/dist/tests/origin-resolve.test.js +124 -0
  186. package/dist/tests/output-baseline.test.js +217 -0
  187. package/dist/tests/output-shapes-unit.test.js +476 -0
  188. package/dist/tests/parallel-search.test.js +272 -0
  189. package/dist/tests/parameter-metadata.test.js +365 -0
  190. package/dist/tests/paths.test.js +177 -0
  191. package/dist/tests/progressive-disclosure.test.js +280 -0
  192. package/dist/tests/proposals.test.js +279 -0
  193. package/dist/tests/proposed-quality.test.js +271 -0
  194. package/dist/tests/provider-registry.test.js +32 -0
  195. package/dist/tests/ranking-regression.test.js +548 -0
  196. package/dist/tests/reflect-propose.test.js +455 -0
  197. package/dist/tests/registry-build-index.test.js +378 -0
  198. package/dist/tests/registry-cli.test.js +290 -0
  199. package/dist/tests/registry-index-v2.test.js +430 -0
  200. package/dist/tests/registry-install.test.js +728 -0
  201. package/dist/tests/registry-providers/parity.test.js +189 -0
  202. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  203. package/dist/tests/registry-providers/static-index.test.js +204 -0
  204. package/dist/tests/registry-resolve.test.js +126 -0
  205. package/dist/tests/registry-search.test.js +723 -0
  206. package/dist/tests/remember-frontmatter.test.js +380 -0
  207. package/dist/tests/remember-unit.test.js +123 -0
  208. package/dist/tests/ripgrep-install.test.js +251 -0
  209. package/dist/tests/ripgrep-resolve.test.js +108 -0
  210. package/dist/tests/ripgrep.test.js +163 -0
  211. package/dist/tests/save-command.test.js +94 -0
  212. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  213. package/dist/tests/scoring-pipeline.test.js +648 -0
  214. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  215. package/dist/tests/self-update.test.js +442 -0
  216. package/dist/tests/semantic-search-e2e.test.js +512 -0
  217. package/dist/tests/semantic-status.test.js +471 -0
  218. package/dist/tests/setup-run.integration.js +877 -0
  219. package/dist/tests/setup-wizard.test.js +198 -0
  220. package/dist/tests/setup.test.js +131 -0
  221. package/dist/tests/source-add.test.js +11 -0
  222. package/dist/tests/source-clone.test.js +254 -0
  223. package/dist/tests/source-manage.test.js +366 -0
  224. package/dist/tests/source-providers/filesystem.test.js +82 -0
  225. package/dist/tests/source-providers/git.test.js +252 -0
  226. package/dist/tests/source-providers/website.test.js +128 -0
  227. package/dist/tests/source-qa-fixes.test.js +268 -0
  228. package/dist/tests/source-registry.test.js +350 -0
  229. package/dist/tests/source-resolve.test.js +100 -0
  230. package/dist/tests/source-source.test.js +221 -0
  231. package/dist/tests/source.test.js +533 -0
  232. package/dist/tests/tar-utils-scan.test.js +73 -0
  233. package/dist/tests/toggle-components.test.js +73 -0
  234. package/dist/tests/usage-telemetry.test.js +265 -0
  235. package/dist/tests/utility-scoring.test.js +558 -0
  236. package/dist/tests/vault-load-error.test.js +78 -0
  237. package/dist/tests/vault-qa-fixes.test.js +194 -0
  238. package/dist/tests/vault.test.js +429 -0
  239. package/dist/tests/vector-search.test.js +608 -0
  240. package/dist/tests/walker.test.js +252 -0
  241. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  242. package/dist/tests/wave2-cluster-d.test.js +180 -0
  243. package/dist/tests/wave2-cluster-e.test.js +179 -0
  244. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  245. package/dist/tests/wiki.test.js +529 -0
  246. package/dist/tests/workflow-cli.test.js +271 -0
  247. package/dist/tests/workflow-markdown.test.js +171 -0
  248. package/dist/tests/workflow-path-escape.test.js +132 -0
  249. package/dist/tests/workflow-qa-fixes.test.js +377 -0
  250. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  251. package/docs/README.md +8 -0
  252. package/docs/migration/release-notes/0.7.0.md +244 -0
  253. package/package.json +2 -2
  254. package/dist/core/warn.js +0 -27
  255. package/dist/output/shapes.js +0 -212
  256. /package/dist/{commands → src/commands}/completions.js +0 -0
  257. /package/dist/{commands → src/commands}/curate.js +0 -0
  258. /package/dist/{commands → src/commands}/info.js +0 -0
  259. /package/dist/{commands → src/commands}/init.js +0 -0
  260. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  261. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  262. /package/dist/{commands → src/commands}/source-add.js +0 -0
  263. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  264. /package/dist/{commands → src/commands}/source-manage.js +0 -0
  265. /package/dist/{commands → src/commands}/vault.js +0 -0
  266. /package/dist/{core → src/core}/asset-registry.js +0 -0
  267. /package/dist/{core → src/core}/frontmatter.js +0 -0
  268. /package/dist/{core → src/core}/markdown.js +0 -0
  269. /package/dist/{core → src/core}/paths.js +0 -0
  270. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  271. /package/dist/{indexer → src/indexer}/matchers.js +0 -0
  272. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  273. /package/dist/{indexer → src/indexer}/search-source.js +0 -0
  274. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  275. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  276. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  277. /package/dist/{integrations → src/integrations}/github.js +0 -0
  278. /package/dist/{llm → src/llm}/embedder.js +0 -0
  279. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  280. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  281. /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
  282. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  283. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  284. /package/dist/{output → src/output}/cli-hints.js +0 -0
  285. /package/dist/{output → src/output}/context.js +0 -0
  286. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  287. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  288. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  289. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  290. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  291. /package/dist/{registry → src/registry}/types.js +0 -0
  292. /package/dist/{setup → src/setup}/detect.js +0 -0
  293. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  294. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  295. /package/dist/{setup → src/setup}/steps.js +0 -0
  296. /package/dist/{sources → src/sources}/include.js +0 -0
  297. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  298. /package/dist/{sources → src/sources}/provider.js +0 -0
  299. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  300. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  301. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  302. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  303. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  304. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  305. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  306. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  307. /package/dist/{sources → src/sources}/resolve.js +0 -0
  308. /package/dist/{sources → src/sources}/types.js +0 -0
  309. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  310. /package/dist/{version.js → src/version.js} +0 -0
  311. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  312. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  313. /package/dist/{workflows → src/workflows}/db.js +0 -0
  314. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  315. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  316. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  317. /package/dist/{workflows → src/workflows}/runs.js +0 -0
  318. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  319. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Unit tests for §6.8 feedback-signal integrity (#244).
3
+ *
4
+ * Coverage:
5
+ * • All four 2×2 quadrants (TP, FP, TN, FN).
6
+ * • Per-asset breakdown when an asset has mixed signals across runs.
7
+ * • `feedback_agreement < 0.80` triggers the warning marker (markdown +
8
+ * structured `warnings[]` JSON entry).
9
+ * • `feedback_coverage` correctly counts runs with feedback dispatched
10
+ * vs total Phase 1 runs.
11
+ * • NaN-safety: zero-feedback asset emits all rates as `null`, never
12
+ * `0` or `NaN`.
13
+ * • Attribution rule (§6.8): a feedback event is attributed to the run
14
+ * that produced it, not to a later run touching the same asset.
15
+ *
16
+ * The metric is a pure function over RunResult[] + feedbackLog[]; no spawn
17
+ * fakes are needed. We build small synthetic streams directly.
18
+ */
19
+ import { describe, expect, test } from "bun:test";
20
+ import { computeFeedbackIntegrity } from "./metrics";
21
+ import { FEEDBACK_AGREEMENT_WARNING_THRESHOLD, renderEvolveReport, renderFeedbackIntegrityTable } from "./report";
22
+ function fakeRun(overrides) {
23
+ return {
24
+ schemaVersion: 1,
25
+ taskId: "t",
26
+ arm: "akm",
27
+ seed: 0,
28
+ model: "m",
29
+ outcome: "pass",
30
+ tokens: { input: 0, output: 0 },
31
+ wallclockMs: 0,
32
+ trajectory: { correctAssetLoaded: null, feedbackRecorded: null },
33
+ events: [],
34
+ verifierStdout: "",
35
+ verifierExitCode: 0,
36
+ assetsLoaded: [],
37
+ ...overrides,
38
+ };
39
+ }
40
+ function fb(overrides) {
41
+ return {
42
+ taskId: "t",
43
+ seed: 0,
44
+ goldRef: "skill:s",
45
+ signal: "positive",
46
+ ok: true,
47
+ ...overrides,
48
+ };
49
+ }
50
+ describe("computeFeedbackIntegrity — 2x2 quadrants", () => {
51
+ test("TP: feedback + on a passed run", () => {
52
+ const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "pass" })] };
53
+ const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "positive" })];
54
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
55
+ expect(m.aggregate.truePositive).toBe(1);
56
+ expect(m.aggregate.falsePositive).toBe(0);
57
+ expect(m.aggregate.trueNegative).toBe(0);
58
+ expect(m.aggregate.falseNegative).toBe(0);
59
+ expect(m.aggregate.feedback_agreement).toBeCloseTo(1);
60
+ expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
61
+ expect(m.perAsset).toHaveLength(1);
62
+ expect(m.perAsset[0].ref).toBe("skill:a");
63
+ expect(m.perAsset[0].truePositive).toBe(1);
64
+ expect(m.perAsset[0].feedback_agreement).toBeCloseTo(1);
65
+ });
66
+ test("FP: feedback + on a failed run", () => {
67
+ const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "fail" })] };
68
+ const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "positive" })];
69
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
70
+ expect(m.aggregate.truePositive).toBe(0);
71
+ expect(m.aggregate.falsePositive).toBe(1);
72
+ expect(m.aggregate.trueNegative).toBe(0);
73
+ expect(m.aggregate.falseNegative).toBe(0);
74
+ expect(m.aggregate.feedback_agreement).toBeCloseTo(0);
75
+ expect(m.aggregate.false_positive_rate).toBeCloseTo(1);
76
+ expect(m.perAsset[0].falsePositive).toBe(1);
77
+ });
78
+ test("TN: feedback - on a failed run", () => {
79
+ const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "fail" })] };
80
+ const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "negative" })];
81
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
82
+ expect(m.aggregate.trueNegative).toBe(1);
83
+ expect(m.aggregate.feedback_agreement).toBeCloseTo(1);
84
+ expect(m.aggregate.false_positive_rate).toBeCloseTo(0);
85
+ expect(m.perAsset[0].trueNegative).toBe(1);
86
+ });
87
+ test("FN: feedback - on a passed run", () => {
88
+ const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "pass" })] };
89
+ const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "negative" })];
90
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
91
+ expect(m.aggregate.falseNegative).toBe(1);
92
+ expect(m.aggregate.feedback_agreement).toBeCloseTo(0);
93
+ expect(m.aggregate.false_negative_rate).toBeCloseTo(1);
94
+ expect(m.perAsset[0].falseNegative).toBe(1);
95
+ });
96
+ });
97
+ describe("computeFeedbackIntegrity — aggregate over mixed quadrants", () => {
98
+ test("computes feedback_agreement and rates correctly across mixed runs", () => {
99
+ // 4 runs covering all four quadrants — exactly one of each.
100
+ const phase1 = {
101
+ akmRuns: [
102
+ fakeRun({ taskId: "tp", seed: 0, outcome: "pass" }),
103
+ fakeRun({ taskId: "fp", seed: 0, outcome: "fail" }),
104
+ fakeRun({ taskId: "tn", seed: 0, outcome: "fail" }),
105
+ fakeRun({ taskId: "fn", seed: 0, outcome: "pass" }),
106
+ ],
107
+ };
108
+ const feedbackLog = [
109
+ fb({ taskId: "tp", seed: 0, goldRef: "skill:tp", signal: "positive" }),
110
+ fb({ taskId: "fp", seed: 0, goldRef: "skill:fp", signal: "positive" }),
111
+ fb({ taskId: "tn", seed: 0, goldRef: "skill:tn", signal: "negative" }),
112
+ fb({ taskId: "fn", seed: 0, goldRef: "skill:fn", signal: "negative" }),
113
+ ];
114
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
115
+ expect(m.aggregate.truePositive).toBe(1);
116
+ expect(m.aggregate.falsePositive).toBe(1);
117
+ expect(m.aggregate.trueNegative).toBe(1);
118
+ expect(m.aggregate.falseNegative).toBe(1);
119
+ expect(m.aggregate.feedback_agreement).toBeCloseTo(0.5); // 2/4
120
+ expect(m.aggregate.false_positive_rate).toBeCloseTo(0.5); // 1 / (1+1)
121
+ expect(m.aggregate.false_negative_rate).toBeCloseTo(0.5); // 1 / (1+1)
122
+ expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
123
+ expect(m.perAsset).toHaveLength(4);
124
+ // Per-asset rows should be sorted by ref
125
+ expect(m.perAsset.map((r) => r.ref)).toEqual(["skill:fn", "skill:fp", "skill:tn", "skill:tp"]);
126
+ });
127
+ });
128
+ describe("computeFeedbackIntegrity — per-asset mixed signals", () => {
129
+ test("aggregates correctly when one asset appears across multiple Phase 1 runs", () => {
130
+ // skill:shared has 2 TP, 1 FP, 1 TN, 1 FN across 5 runs.
131
+ const phase1 = {
132
+ akmRuns: [
133
+ fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
134
+ fakeRun({ taskId: "t", seed: 1, outcome: "pass" }),
135
+ fakeRun({ taskId: "t", seed: 2, outcome: "fail" }),
136
+ fakeRun({ taskId: "t", seed: 3, outcome: "fail" }),
137
+ fakeRun({ taskId: "t", seed: 4, outcome: "pass" }),
138
+ ],
139
+ };
140
+ const feedbackLog = [
141
+ fb({ taskId: "t", seed: 0, goldRef: "skill:shared", signal: "positive" }), // TP
142
+ fb({ taskId: "t", seed: 1, goldRef: "skill:shared", signal: "positive" }), // TP
143
+ fb({ taskId: "t", seed: 2, goldRef: "skill:shared", signal: "positive" }), // FP
144
+ fb({ taskId: "t", seed: 3, goldRef: "skill:shared", signal: "negative" }), // TN
145
+ fb({ taskId: "t", seed: 4, goldRef: "skill:shared", signal: "negative" }), // FN
146
+ ];
147
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
148
+ expect(m.perAsset).toHaveLength(1);
149
+ const row = m.perAsset[0];
150
+ expect(row.ref).toBe("skill:shared");
151
+ expect(row.truePositive).toBe(2);
152
+ expect(row.falsePositive).toBe(1);
153
+ expect(row.trueNegative).toBe(1);
154
+ expect(row.falseNegative).toBe(1);
155
+ expect(row.feedback_agreement).toBeCloseTo(3 / 5);
156
+ expect(row.false_positive_rate).toBeCloseTo(1 / 2); // FP / (FP+TN) = 1/2
157
+ expect(row.false_negative_rate).toBeCloseTo(1 / 3); // FN / (FN+TP) = 1/3
158
+ });
159
+ });
160
+ describe("computeFeedbackIntegrity — attribution rule", () => {
161
+ test("attributes feedback to the run that produced it, not a later run touching the same asset", () => {
162
+ // skill:contested appears across two Phase 1 runs:
163
+ // run #0: passed, feedback + → TP
164
+ // run #1: failed, feedback + → FP
165
+ // The naive (wrong) implementation would conflate both events with
166
+ // run #1's outcome and label both as FP. The correct implementation
167
+ // joins each event to its own (taskId, seed) → gets one TP, one FP.
168
+ const phase1 = {
169
+ akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
170
+ };
171
+ const feedbackLog = [
172
+ fb({ taskId: "t", seed: 0, goldRef: "skill:contested", signal: "positive" }),
173
+ fb({ taskId: "t", seed: 1, goldRef: "skill:contested", signal: "positive" }),
174
+ ];
175
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
176
+ expect(m.aggregate.truePositive).toBe(1);
177
+ expect(m.aggregate.falsePositive).toBe(1);
178
+ expect(m.aggregate.trueNegative).toBe(0);
179
+ expect(m.aggregate.falseNegative).toBe(0);
180
+ expect(m.perAsset[0].truePositive).toBe(1);
181
+ expect(m.perAsset[0].falsePositive).toBe(1);
182
+ });
183
+ });
184
+ describe("computeFeedbackIntegrity — feedback_coverage", () => {
185
+ test("counts runs with feedback dispatched vs total Phase 1 runs", () => {
186
+ // 4 phase-1 runs, only 2 had feedback dispatched.
187
+ const phase1 = {
188
+ akmRuns: [
189
+ fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
190
+ fakeRun({ taskId: "t", seed: 1, outcome: "fail" }),
191
+ fakeRun({ taskId: "t", seed: 2, outcome: "harness_error" }),
192
+ fakeRun({ taskId: "t", seed: 3, outcome: "budget_exceeded" }),
193
+ ],
194
+ };
195
+ const feedbackLog = [
196
+ fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
197
+ fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "negative" }),
198
+ ];
199
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
200
+ expect(m.aggregate.feedback_coverage).toBeCloseTo(0.5); // 2 of 4
201
+ });
202
+ test("zero coverage when no feedback dispatched", () => {
203
+ const phase1 = { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" })] };
204
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog: [] });
205
+ expect(m.aggregate.feedback_coverage).toBe(0);
206
+ expect(m.aggregate.feedback_agreement).toBe(0);
207
+ expect(m.perAsset).toEqual([]);
208
+ });
209
+ test("zero coverage and zero runs returns 0 (not NaN)", () => {
210
+ const m = computeFeedbackIntegrity({ phase1: { akmRuns: [] }, feedbackLog: [] });
211
+ expect(m.aggregate.feedback_coverage).toBe(0);
212
+ expect(m.aggregate.feedback_agreement).toBe(0);
213
+ expect(m.aggregate.false_positive_rate).toBe(0);
214
+ expect(m.aggregate.false_negative_rate).toBe(0);
215
+ expect(Number.isFinite(m.aggregate.feedback_coverage)).toBe(true);
216
+ expect(Number.isFinite(m.aggregate.feedback_agreement)).toBe(true);
217
+ });
218
+ });
219
+ describe("computeFeedbackIntegrity — NaN safety", () => {
220
+ test("per-asset row with FP+TN === 0 emits null false_positive_rate (only positive feedback on passes)", () => {
221
+ const phase1 = {
222
+ akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "pass" })],
223
+ };
224
+ const feedbackLog = [
225
+ fb({ taskId: "t", seed: 0, goldRef: "skill:only-tp", signal: "positive" }),
226
+ fb({ taskId: "t", seed: 1, goldRef: "skill:only-tp", signal: "positive" }),
227
+ ];
228
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
229
+ const row = m.perAsset[0];
230
+ expect(row.feedback_agreement).toBeCloseTo(1);
231
+ expect(row.false_positive_rate).toBeNull(); // FP+TN === 0
232
+ expect(row.false_negative_rate).toBeCloseTo(0); // FN/(FN+TP) = 0/2 = 0
233
+ });
234
+ test("per-asset row with FN+TP === 0 emits null false_negative_rate (only negative feedback on fails)", () => {
235
+ const phase1 = {
236
+ akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "fail" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
237
+ };
238
+ const feedbackLog = [
239
+ fb({ taskId: "t", seed: 0, goldRef: "skill:only-tn", signal: "negative" }),
240
+ fb({ taskId: "t", seed: 1, goldRef: "skill:only-tn", signal: "negative" }),
241
+ ];
242
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
243
+ const row = m.perAsset[0];
244
+ expect(row.feedback_agreement).toBeCloseTo(1);
245
+ expect(row.false_negative_rate).toBeNull(); // FN+TP === 0
246
+ expect(row.false_positive_rate).toBeCloseTo(0); // FP/(FP+TN) = 0/2 = 0
247
+ });
248
+ test("ok=false feedback events are excluded from the matrix but still count toward coverage", () => {
249
+ const phase1 = {
250
+ akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
251
+ };
252
+ const feedbackLog = [
253
+ fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive", ok: true }),
254
+ fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "negative", ok: false }),
255
+ ];
256
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
257
+ // Only the ok=true entry contributes to the matrix (TP=1).
258
+ expect(m.aggregate.truePositive).toBe(1);
259
+ expect(m.aggregate.trueNegative).toBe(0);
260
+ // But coverage counts both attempts.
261
+ expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
262
+ });
263
+ test("harness_error runs are excluded from the matrix even with a stamped feedback event", () => {
264
+ const phase1 = { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "harness_error" })] };
265
+ const feedbackLog = [fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" })];
266
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
267
+ expect(m.aggregate.truePositive).toBe(0);
268
+ expect(m.aggregate.falsePositive).toBe(0);
269
+ expect(m.perAsset).toEqual([]);
270
+ });
271
+ test("feedback for a run not present in akmRuns is silently dropped", () => {
272
+ const phase1 = { akmRuns: [fakeRun({ taskId: "real", seed: 0, outcome: "pass" })] };
273
+ const feedbackLog = [fb({ taskId: "ghost", seed: 99, goldRef: "skill:a", signal: "positive" })];
274
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
275
+ expect(m.aggregate.truePositive).toBe(0);
276
+ expect(m.perAsset).toEqual([]);
277
+ // Coverage still records the dispatch attempt — operator wanted feedback.
278
+ expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
279
+ });
280
+ });
281
+ // ── Render-side coverage ───────────────────────────────────────────────────
282
+ function emptyUtilityReport() {
283
+ // Build a minimal §13.3-shaped utility report. The renderer reads
284
+ // many subfields; we stub them to safe zeros.
285
+ return {
286
+ timestamp: "2026-04-27T00:00:00Z",
287
+ branch: "test",
288
+ commit: "deadbee",
289
+ model: "m",
290
+ corpus: { domains: 0, tasks: 0, slice: "all", seedsPerArm: 1 },
291
+ aggregateNoakm: { passRate: 0, tokensPerPass: 0, wallclockMs: 0 },
292
+ aggregateAkm: { passRate: 0, tokensPerPass: 0, wallclockMs: 0 },
293
+ aggregateDelta: {
294
+ passRate: 0,
295
+ tokensPerPass: 0,
296
+ wallclockMs: 0,
297
+ },
298
+ trajectoryAkm: {
299
+ correctAssetLoaded: null,
300
+ feedbackRecorded: 0,
301
+ },
302
+ failureModes: { byLabel: {}, byTask: {} },
303
+ tasks: [],
304
+ warnings: [],
305
+ akmRuns: [],
306
+ taskMetadata: [],
307
+ goldRankRecords: [],
308
+ };
309
+ }
310
+ function evolveInputWith(metrics) {
311
+ return {
312
+ timestamp: "2026-04-27T00:00:00Z",
313
+ branch: "test",
314
+ commit: "deadbee",
315
+ model: "m",
316
+ domain: "test",
317
+ seedsPerArm: 1,
318
+ proposals: { rows: [], totalProposals: 0, totalAccepted: 0, acceptanceRate: 0, lintPassRate: 0 },
319
+ longitudinal: {
320
+ improvementSlope: 0.1,
321
+ overSyntheticLift: 0.05,
322
+ degradationCount: 0,
323
+ degradations: [],
324
+ prePassRate: 0.5,
325
+ postPassRate: 0.6,
326
+ syntheticPassRate: 0.55,
327
+ },
328
+ arms: { pre: emptyUtilityReport(), post: emptyUtilityReport(), synthetic: emptyUtilityReport() },
329
+ warnings: [],
330
+ ...(metrics ? { feedbackIntegrity: metrics } : {}),
331
+ };
332
+ }
333
+ describe("renderFeedbackIntegrityTable", () => {
334
+ test("emits aggregate matrix + per-asset rows", () => {
335
+ const phase1 = {
336
+ akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
337
+ };
338
+ const feedbackLog = [
339
+ fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
340
+ fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "negative" }),
341
+ ];
342
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
343
+ const md = renderFeedbackIntegrityTable(m);
344
+ expect(md).toContain("Feedback-signal integrity");
345
+ expect(md).toContain("feedback_agreement | 1.00");
346
+ expect(md).toContain("feedback_coverage | 1.00");
347
+ expect(md).toContain("`skill:a`");
348
+ });
349
+ test("renders n/a when a per-asset rate is null", () => {
350
+ const phase1 = { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" })] };
351
+ const feedbackLog = [fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" })];
352
+ const m = computeFeedbackIntegrity({ phase1, feedbackLog });
353
+ const md = renderFeedbackIntegrityTable(m);
354
+ // Only TP — false_positive_rate denom is 0 → null → "n/a".
355
+ expect(md).toContain("n/a");
356
+ });
357
+ test("renders 'No feedback events recorded' when perAsset is empty", () => {
358
+ const m = {
359
+ aggregate: {
360
+ truePositive: 0,
361
+ falsePositive: 0,
362
+ trueNegative: 0,
363
+ falseNegative: 0,
364
+ feedback_agreement: 0,
365
+ false_positive_rate: 0,
366
+ false_negative_rate: 0,
367
+ feedback_coverage: 0,
368
+ },
369
+ perAsset: [],
370
+ };
371
+ expect(renderFeedbackIntegrityTable(m)).toContain("No feedback events recorded");
372
+ });
373
+ });
374
+ describe("renderEvolveReport — feedback_agreement headline + warning marker", () => {
375
+ test("places real feedback_agreement after improvement_slope when metrics provided", () => {
376
+ const metrics = computeFeedbackIntegrity({
377
+ phase1: { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" })] },
378
+ feedbackLog: [fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" })],
379
+ });
380
+ const { markdown, json } = renderEvolveReport(evolveInputWith(metrics));
381
+ // feedback_agreement is on a line directly after improvement_slope.
382
+ const slopeIdx = markdown.indexOf("improvement_slope:");
383
+ const agreementIdx = markdown.indexOf("feedback_agreement:");
384
+ expect(slopeIdx).toBeGreaterThanOrEqual(0);
385
+ expect(agreementIdx).toBeGreaterThan(slopeIdx);
386
+ expect(markdown).toContain("feedback_agreement: 1.00");
387
+ expect(markdown).not.toContain("pending (#244)");
388
+ // JSON envelope carries `feedback_integrity` as a top-level key.
389
+ const parsed = json;
390
+ expect(parsed.feedback_integrity).toBeDefined();
391
+ expect(parsed.warnings.some((w) => w.startsWith("feedback_agreement_below_threshold"))).toBe(false);
392
+ });
393
+ test("placeholder remains when metrics omitted (legacy path)", () => {
394
+ const { markdown, json } = renderEvolveReport(evolveInputWith(undefined));
395
+ expect(markdown).toContain("_feedback_agreement: pending (#244)_");
396
+ const parsed = json;
397
+ expect(parsed.feedback_integrity).toBeUndefined();
398
+ });
399
+ test("agreement < 0.80 prepends warning marker to markdown and structured warnings[]", () => {
400
+ // 1 TP + 4 FP → agreement = 1/5 = 0.20.
401
+ const phase1 = {
402
+ akmRuns: [
403
+ fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
404
+ fakeRun({ taskId: "t", seed: 1, outcome: "fail" }),
405
+ fakeRun({ taskId: "t", seed: 2, outcome: "fail" }),
406
+ fakeRun({ taskId: "t", seed: 3, outcome: "fail" }),
407
+ fakeRun({ taskId: "t", seed: 4, outcome: "fail" }),
408
+ ],
409
+ };
410
+ const feedbackLog = [
411
+ fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
412
+ fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "positive" }),
413
+ fb({ taskId: "t", seed: 2, goldRef: "skill:a", signal: "positive" }),
414
+ fb({ taskId: "t", seed: 3, goldRef: "skill:a", signal: "positive" }),
415
+ fb({ taskId: "t", seed: 4, goldRef: "skill:a", signal: "positive" }),
416
+ ];
417
+ const metrics = computeFeedbackIntegrity({ phase1, feedbackLog });
418
+ expect(metrics.aggregate.feedback_agreement).toBeCloseTo(0.2);
419
+ expect(metrics.aggregate.feedback_agreement).toBeLessThan(FEEDBACK_AGREEMENT_WARNING_THRESHOLD);
420
+ const { markdown, json } = renderEvolveReport(evolveInputWith(metrics));
421
+ // Marker appears above the headline, not after it.
422
+ const warnIdx = markdown.indexOf("feedback_agreement = 0.20");
423
+ const slopeIdx = markdown.indexOf("**improvement_slope:");
424
+ expect(warnIdx).toBeGreaterThanOrEqual(0);
425
+ expect(warnIdx).toBeLessThan(slopeIdx);
426
+ expect(markdown).toContain("Track B headline numbers");
427
+ // Structured warning surfaces in the JSON envelope.
428
+ const parsed = json;
429
+ expect(parsed.warnings.some((w) => w.startsWith("feedback_agreement_below_threshold"))).toBe(true);
430
+ });
431
+ test("agreement at exactly 0.80 does NOT trigger the warning marker", () => {
432
+ // 4 TP + 1 FP → agreement = 4/5 = 0.80 exactly.
433
+ const phase1 = {
434
+ akmRuns: [
435
+ fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
436
+ fakeRun({ taskId: "t", seed: 1, outcome: "pass" }),
437
+ fakeRun({ taskId: "t", seed: 2, outcome: "pass" }),
438
+ fakeRun({ taskId: "t", seed: 3, outcome: "pass" }),
439
+ fakeRun({ taskId: "t", seed: 4, outcome: "fail" }),
440
+ ],
441
+ };
442
+ const feedbackLog = [
443
+ fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
444
+ fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "positive" }),
445
+ fb({ taskId: "t", seed: 2, goldRef: "skill:a", signal: "positive" }),
446
+ fb({ taskId: "t", seed: 3, goldRef: "skill:a", signal: "positive" }),
447
+ fb({ taskId: "t", seed: 4, goldRef: "skill:a", signal: "positive" }),
448
+ ];
449
+ const metrics = computeFeedbackIntegrity({ phase1, feedbackLog });
450
+ expect(metrics.aggregate.feedback_agreement).toBeCloseTo(0.8);
451
+ const { markdown, json } = renderEvolveReport(evolveInputWith(metrics));
452
+ expect(markdown).not.toContain("Track B headline numbers");
453
+ const parsed = json;
454
+ expect(parsed.warnings.some((w) => w.startsWith("feedback_agreement_below_threshold"))).toBe(false);
455
+ });
456
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Leakage smoke test for the seeded bench corpus (spec §7.4).
3
+ *
4
+ * For every task that declares a `gold_ref` of the form `skill:<name>`,
5
+ * locate the SKILL.md inside the named fixture stash and assert that the
6
+ * verifier's *structural assertions* do not appear verbatim in the gold-ref
7
+ * content. The gold ref is allowed (and expected) to discuss the topic in
8
+ * general terms — what it must NOT do is hand the agent a copy-pasteable
9
+ * fragment that satisfies the verifier directly.
10
+ *
11
+ * The check extracts:
12
+ * • for `regex` verifiers — the literal segments of `expected_match`
13
+ * between regex meta-characters (these are the substrings the agent
14
+ * must produce);
15
+ * • for `pytest` verifiers — Python-style structural assertion paths and
16
+ * dictionary lookups (e.g., `services.redis.healthcheck.test`,
17
+ * `redis["healthcheck"]["test"]`);
18
+ * • for `script` (shell) verifiers — single-quoted `grep` patterns and
19
+ * `jq -e` expressions, which encode the exact assertion shape.
20
+ *
21
+ * Each fragment is checked individually. Lone tokens that legitimately
22
+ * appear in any reasonable description of the topic (e.g., `redis-cli`,
23
+ * `akm`, `bridge`, `feedback`) are filtered out by a minimum-length and
24
+ * minimum-token-count rule.
25
+ */
26
+ import { describe, expect, test } from "bun:test";
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+ import { getTasksRoot, listTasks } from "./corpus";
30
+ const STASHES_ROOT = path.resolve(getTasksRoot(), "..", "..", "stashes");
31
+ /** Resolve `skill:<name>` against the named stash; returns SKILL.md path or `undefined`. */
32
+ function resolveGoldRefPath(stashName, goldRef) {
33
+ const match = /^skill:([a-z0-9][a-z0-9-]*)$/.exec(goldRef);
34
+ if (!match)
35
+ return undefined;
36
+ const skillDir = path.join(STASHES_ROOT, stashName, "skills", match[1]);
37
+ const skillFile = path.join(skillDir, "SKILL.md");
38
+ return fs.existsSync(skillFile) ? skillFile : undefined;
39
+ }
40
+ /**
41
+ * Pull the literal segments out of a regex pattern. Splits on regex
42
+ * meta-characters and discards short fragments. The remaining strings are
43
+ * what the agent's stdout must contain — and therefore what the gold ref
44
+ * must NOT spell out verbatim.
45
+ */
46
+ function regexLiterals(pattern) {
47
+ return pattern
48
+ .split(/[.*+?^${}()|[\]\\]/)
49
+ .map((s) => s.trim())
50
+ .filter((s) => s.length >= 6 && s.includes(" "));
51
+ }
52
+ /** Pull structural assertion fragments out of a pytest verifier file. */
53
+ function pytestStructuralFragments(text) {
54
+ const out = new Set();
55
+ // Subscript chains like compose["services"]["redis"]["healthcheck"]["test"].
56
+ const subscriptRe = /(?:\["[a-z0-9_]+"\]){2,}/g;
57
+ for (const m of text.matchAll(subscriptRe))
58
+ out.add(m[0]);
59
+ // Dotted attribute paths used in error messages, e.g. services.redis.healthcheck.test.
60
+ const dottedRe = /[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){2,}/g;
61
+ for (const m of text.matchAll(dottedRe))
62
+ out.add(m[0]);
63
+ return [...out];
64
+ }
65
+ /** Pull shell-verifier assertions: single-quoted greps and jq -e expressions. */
66
+ function shellAssertionFragments(text) {
67
+ const out = new Set();
68
+ // grep -q '<pattern>' or grep -qi '<pattern>'.
69
+ const grepRe = /grep\s+-[a-zA-Z]+\s+'([^']{4,})'/g;
70
+ for (const m of text.matchAll(grepRe))
71
+ out.add(m[1]);
72
+ // jq -e '<expr>'.
73
+ const jqRe = /jq\s+-e\s+'([^']{4,})'/g;
74
+ for (const m of text.matchAll(jqRe))
75
+ out.add(m[1]);
76
+ return [...out];
77
+ }
78
+ function readVerifierFiles(task) {
79
+ let combined = "";
80
+ if (task.verifier === "pytest") {
81
+ const testsDir = path.join(task.taskDir, "tests");
82
+ if (fs.existsSync(testsDir)) {
83
+ for (const entry of fs.readdirSync(testsDir)) {
84
+ if (entry.endsWith(".py"))
85
+ combined += `${fs.readFileSync(path.join(testsDir, entry), "utf8")}\n`;
86
+ }
87
+ }
88
+ }
89
+ else if (task.verifier === "script") {
90
+ const verify = path.join(task.taskDir, "verify.sh");
91
+ if (fs.existsSync(verify))
92
+ combined += fs.readFileSync(verify, "utf8");
93
+ }
94
+ return combined;
95
+ }
96
+ describe("gold-ref leakage check", () => {
97
+ const tasks = listTasks().filter((t) => t.goldRef);
98
+ test("at least one task ships with a gold_ref", () => {
99
+ expect(tasks.length).toBeGreaterThan(0);
100
+ });
101
+ for (const task of tasks) {
102
+ test(`${task.id}: verifier text does not appear in gold-ref content`, () => {
103
+ const goldRef = task.goldRef;
104
+ const goldPath = resolveGoldRefPath(task.stash, goldRef);
105
+ // A declared gold_ref MUST resolve to an existing fixture asset. Silent
106
+ // skipping here previously masked typos and stash-name drift; we now
107
+ // fail loudly so the corpus author is forced to fix the reference.
108
+ if (!goldPath) {
109
+ throw new Error(`${task.id}: gold_ref "${goldRef}" against stash "${task.stash}" did not resolve to a SKILL.md under tests/fixtures/stashes/. Fix the gold_ref, fix the stash name, or remove the gold_ref.`);
110
+ }
111
+ const goldContent = fs.readFileSync(goldPath, "utf8");
112
+ const fragments = [];
113
+ if (task.verifier === "regex" && task.expectedMatch) {
114
+ fragments.push(...regexLiterals(task.expectedMatch));
115
+ }
116
+ else {
117
+ const verifierText = readVerifierFiles(task);
118
+ fragments.push(...pytestStructuralFragments(verifierText));
119
+ fragments.push(...shellAssertionFragments(verifierText));
120
+ }
121
+ const leaked = fragments.filter((frag) => goldContent.includes(frag));
122
+ expect(leaked).toEqual([]);
123
+ });
124
+ }
125
+ });