akm-cli 0.7.0-rc1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/dist/{src/cli.js → cli.js} +100 -16
  2. package/dist/{src/commands → commands}/config-cli.js +42 -0
  3. package/dist/{src/commands → commands}/history.js +78 -7
  4. package/dist/{src/commands → commands}/registry-search.js +69 -6
  5. package/dist/{src/commands → commands}/search.js +30 -3
  6. package/dist/{src/commands → commands}/show.js +29 -0
  7. package/dist/{src/commands → commands}/source-add.js +5 -1
  8. package/dist/{src/commands → commands}/source-manage.js +7 -1
  9. package/dist/{src/core → core}/config.js +28 -0
  10. package/dist/{src/indexer → indexer}/db-search.js +1 -0
  11. package/dist/{src/indexer → indexer}/indexer.js +16 -2
  12. package/dist/{src/indexer → indexer}/matchers.js +1 -1
  13. package/dist/{src/indexer → indexer}/search-source.js +4 -2
  14. package/dist/{src/integrations → integrations}/agent/profiles.js +1 -1
  15. package/dist/{src/integrations → integrations}/agent/spawn.js +67 -16
  16. package/dist/{src/integrations → integrations}/github.js +9 -3
  17. package/dist/{src/llm → llm}/embedders/remote.js +37 -3
  18. package/dist/{src/output → output}/cli-hints.js +15 -2
  19. package/dist/{src/output → output}/renderers.js +3 -1
  20. package/dist/{src/output → output}/shapes.js +8 -1
  21. package/dist/{src/output → output}/text.js +156 -3
  22. package/dist/{src/registry → registry}/build-index.js +5 -4
  23. package/dist/{src/registry → registry}/providers/static-index.js +3 -1
  24. package/dist/{src/setup → setup}/setup.js +9 -0
  25. package/dist/{src/wiki → wiki}/wiki.js +54 -6
  26. package/dist/{src/workflows → workflows}/runs.js +37 -3
  27. package/package.json +8 -8
  28. package/dist/tests/add-website-source.test.js +0 -119
  29. package/dist/tests/agent/agent-config-loader.test.js +0 -70
  30. package/dist/tests/agent/agent-config.test.js +0 -221
  31. package/dist/tests/agent/agent-detect.test.js +0 -100
  32. package/dist/tests/agent/agent-spawn.test.js +0 -234
  33. package/dist/tests/agent-output.test.js +0 -186
  34. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +0 -103
  35. package/dist/tests/architecture/agent-spawn-seam.test.js +0 -193
  36. package/dist/tests/architecture/llm-stateless-seam.test.js +0 -112
  37. package/dist/tests/asset-ref.test.js +0 -192
  38. package/dist/tests/asset-registry.test.js +0 -103
  39. package/dist/tests/asset-spec.test.js +0 -241
  40. package/dist/tests/bench/attribution.test.js +0 -995
  41. package/dist/tests/bench/cleanup-sigint.test.js +0 -83
  42. package/dist/tests/bench/cleanup.js +0 -203
  43. package/dist/tests/bench/cleanup.test.js +0 -166
  44. package/dist/tests/bench/cli.js +0 -683
  45. package/dist/tests/bench/cli.test.js +0 -177
  46. package/dist/tests/bench/compare.test.js +0 -556
  47. package/dist/tests/bench/corpus.js +0 -314
  48. package/dist/tests/bench/corpus.test.js +0 -258
  49. package/dist/tests/bench/driver.js +0 -346
  50. package/dist/tests/bench/driver.test.js +0 -443
  51. package/dist/tests/bench/evolve-metrics.js +0 -179
  52. package/dist/tests/bench/evolve-metrics.test.js +0 -187
  53. package/dist/tests/bench/evolve.js +0 -580
  54. package/dist/tests/bench/evolve.test.js +0 -616
  55. package/dist/tests/bench/failure-modes.test.js +0 -300
  56. package/dist/tests/bench/feedback-integrity.test.js +0 -456
  57. package/dist/tests/bench/leakage.test.js +0 -125
  58. package/dist/tests/bench/learning-curve.test.js +0 -133
  59. package/dist/tests/bench/metrics.js +0 -2319
  60. package/dist/tests/bench/metrics.test.js +0 -1144
  61. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +0 -43
  62. package/dist/tests/bench/report.js +0 -1821
  63. package/dist/tests/bench/report.test.js +0 -989
  64. package/dist/tests/bench/runner.js +0 -536
  65. package/dist/tests/bench/runner.test.js +0 -958
  66. package/dist/tests/bench/search-bridge.test.js +0 -331
  67. package/dist/tests/bench/tmp.js +0 -41
  68. package/dist/tests/bench/trajectory.js +0 -116
  69. package/dist/tests/bench/trajectory.test.js +0 -127
  70. package/dist/tests/bench/verifier.js +0 -109
  71. package/dist/tests/bench/verifier.test.js +0 -118
  72. package/dist/tests/bench/workflow-evaluator.js +0 -557
  73. package/dist/tests/bench/workflow-evaluator.test.js +0 -421
  74. package/dist/tests/bench/workflow-spec.js +0 -358
  75. package/dist/tests/bench/workflow-spec.test.js +0 -363
  76. package/dist/tests/bench/workflow-trace.js +0 -438
  77. package/dist/tests/bench/workflow-trace.test.js +0 -254
  78. package/dist/tests/benchmark-search-quality.js +0 -536
  79. package/dist/tests/benchmark-suite.js +0 -1441
  80. package/dist/tests/capture-cli.test.js +0 -112
  81. package/dist/tests/cli-errors.test.js +0 -203
  82. package/dist/tests/commands/events.test.js +0 -370
  83. package/dist/tests/commands/history.test.js +0 -223
  84. package/dist/tests/commands/import.test.js +0 -103
  85. package/dist/tests/commands/proposal-cli.test.js +0 -209
  86. package/dist/tests/commands/reflect-propose-cli.test.js +0 -333
  87. package/dist/tests/commands/remember.test.js +0 -97
  88. package/dist/tests/commands/scope-flags.test.js +0 -300
  89. package/dist/tests/commands/search.test.js +0 -537
  90. package/dist/tests/commands/show-indexer-parity.test.js +0 -117
  91. package/dist/tests/commands/show.test.js +0 -294
  92. package/dist/tests/common.test.js +0 -266
  93. package/dist/tests/completions.test.js +0 -142
  94. package/dist/tests/config-cli.test.js +0 -193
  95. package/dist/tests/config-llm-features.test.js +0 -139
  96. package/dist/tests/config.test.js +0 -544
  97. package/dist/tests/contracts/migration-baseline.test.js +0 -43
  98. package/dist/tests/contracts/reflect-propose-envelope.test.js +0 -139
  99. package/dist/tests/contracts/spec-helpers.js +0 -46
  100. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +0 -228
  101. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +0 -56
  102. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +0 -34
  103. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +0 -94
  104. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +0 -39
  105. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +0 -44
  106. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +0 -47
  107. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +0 -40
  108. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +0 -58
  109. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +0 -34
  110. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +0 -75
  111. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +0 -36
  112. package/dist/tests/core/write-source.test.js +0 -366
  113. package/dist/tests/curate-command.test.js +0 -87
  114. package/dist/tests/db-scoring.test.js +0 -201
  115. package/dist/tests/db.test.js +0 -654
  116. package/dist/tests/distill-cli-flag.test.js +0 -208
  117. package/dist/tests/distill.test.js +0 -515
  118. package/dist/tests/docker-install.test.js +0 -120
  119. package/dist/tests/e2e.test.js +0 -1398
  120. package/dist/tests/embedder.test.js +0 -340
  121. package/dist/tests/embedding-model-config.test.js +0 -379
  122. package/dist/tests/feedback-command.test.js +0 -172
  123. package/dist/tests/file-context.test.js +0 -552
  124. package/dist/tests/fixtures/scripts/git/summarize-diff.js +0 -9
  125. package/dist/tests/fixtures/scripts/lint/eslint-check.js +0 -7
  126. package/dist/tests/fixtures/stashes/load.js +0 -166
  127. package/dist/tests/fixtures/stashes/load.test.js +0 -88
  128. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +0 -12
  129. package/dist/tests/frontmatter.test.js +0 -190
  130. package/dist/tests/fts-field-weighting.test.js +0 -254
  131. package/dist/tests/fuzzy-search.test.js +0 -230
  132. package/dist/tests/git-provider-clone.test.js +0 -45
  133. package/dist/tests/github.test.js +0 -161
  134. package/dist/tests/graph-boost-ranking.test.js +0 -305
  135. package/dist/tests/graph-extraction.test.js +0 -282
  136. package/dist/tests/helpers/usage-events.js +0 -8
  137. package/dist/tests/index-pass-llm.test.js +0 -161
  138. package/dist/tests/indexer.test.js +0 -559
  139. package/dist/tests/info-command.test.js +0 -166
  140. package/dist/tests/init.test.js +0 -69
  141. package/dist/tests/install-script.test.js +0 -246
  142. package/dist/tests/integration/agent-real-profile.test.js +0 -94
  143. package/dist/tests/issue-36-repro.test.js +0 -304
  144. package/dist/tests/issues-191-194.test.js +0 -160
  145. package/dist/tests/lesson-lint.test.js +0 -111
  146. package/dist/tests/llm-client.test.js +0 -115
  147. package/dist/tests/llm-feature-gate.test.js +0 -151
  148. package/dist/tests/llm.test.js +0 -139
  149. package/dist/tests/lockfile.test.js +0 -216
  150. package/dist/tests/manifest.test.js +0 -205
  151. package/dist/tests/markdown.test.js +0 -126
  152. package/dist/tests/matchers-unit.test.js +0 -189
  153. package/dist/tests/memory-inference.test.js +0 -299
  154. package/dist/tests/merge-scoring.test.js +0 -136
  155. package/dist/tests/metadata.test.js +0 -313
  156. package/dist/tests/migration-help.test.js +0 -89
  157. package/dist/tests/origin-resolve.test.js +0 -124
  158. package/dist/tests/output-baseline.test.js +0 -217
  159. package/dist/tests/output-shapes-unit.test.js +0 -476
  160. package/dist/tests/parallel-search.test.js +0 -272
  161. package/dist/tests/parameter-metadata.test.js +0 -365
  162. package/dist/tests/paths.test.js +0 -177
  163. package/dist/tests/progressive-disclosure.test.js +0 -280
  164. package/dist/tests/proposals.test.js +0 -279
  165. package/dist/tests/proposed-quality.test.js +0 -271
  166. package/dist/tests/provider-registry.test.js +0 -32
  167. package/dist/tests/ranking-regression.test.js +0 -548
  168. package/dist/tests/reflect-propose.test.js +0 -455
  169. package/dist/tests/registry-build-index.test.js +0 -378
  170. package/dist/tests/registry-cli.test.js +0 -290
  171. package/dist/tests/registry-index-v2.test.js +0 -430
  172. package/dist/tests/registry-install.test.js +0 -728
  173. package/dist/tests/registry-providers/parity.test.js +0 -189
  174. package/dist/tests/registry-providers/skills-sh.test.js +0 -309
  175. package/dist/tests/registry-providers/static-index.test.js +0 -204
  176. package/dist/tests/registry-resolve.test.js +0 -126
  177. package/dist/tests/registry-search.test.js +0 -723
  178. package/dist/tests/remember-frontmatter.test.js +0 -380
  179. package/dist/tests/remember-unit.test.js +0 -123
  180. package/dist/tests/ripgrep-install.test.js +0 -251
  181. package/dist/tests/ripgrep-resolve.test.js +0 -108
  182. package/dist/tests/ripgrep.test.js +0 -163
  183. package/dist/tests/save-command.test.js +0 -94
  184. package/dist/tests/save-trust-qa-fixes.test.js +0 -270
  185. package/dist/tests/scoring-pipeline.test.js +0 -648
  186. package/dist/tests/search-include-proposed-cli.test.js +0 -118
  187. package/dist/tests/self-update.test.js +0 -442
  188. package/dist/tests/semantic-search-e2e.test.js +0 -512
  189. package/dist/tests/semantic-status.test.js +0 -471
  190. package/dist/tests/setup-run.integration.js +0 -877
  191. package/dist/tests/setup-wizard.test.js +0 -198
  192. package/dist/tests/setup.test.js +0 -131
  193. package/dist/tests/source-add.test.js +0 -11
  194. package/dist/tests/source-clone.test.js +0 -254
  195. package/dist/tests/source-manage.test.js +0 -366
  196. package/dist/tests/source-providers/filesystem.test.js +0 -82
  197. package/dist/tests/source-providers/git.test.js +0 -252
  198. package/dist/tests/source-providers/website.test.js +0 -128
  199. package/dist/tests/source-qa-fixes.test.js +0 -268
  200. package/dist/tests/source-registry.test.js +0 -350
  201. package/dist/tests/source-resolve.test.js +0 -100
  202. package/dist/tests/source-source.test.js +0 -221
  203. package/dist/tests/source.test.js +0 -533
  204. package/dist/tests/tar-utils-scan.test.js +0 -73
  205. package/dist/tests/toggle-components.test.js +0 -73
  206. package/dist/tests/usage-telemetry.test.js +0 -265
  207. package/dist/tests/utility-scoring.test.js +0 -558
  208. package/dist/tests/vault-load-error.test.js +0 -78
  209. package/dist/tests/vault-qa-fixes.test.js +0 -194
  210. package/dist/tests/vault.test.js +0 -429
  211. package/dist/tests/vector-search.test.js +0 -608
  212. package/dist/tests/walker.test.js +0 -252
  213. package/dist/tests/wave2-cluster-bc.test.js +0 -228
  214. package/dist/tests/wave2-cluster-d.test.js +0 -180
  215. package/dist/tests/wave2-cluster-e.test.js +0 -179
  216. package/dist/tests/wiki-qa-fixes.test.js +0 -270
  217. package/dist/tests/wiki.test.js +0 -529
  218. package/dist/tests/workflow-cli.test.js +0 -271
  219. package/dist/tests/workflow-markdown.test.js +0 -171
  220. package/dist/tests/workflow-path-escape.test.js +0 -132
  221. package/dist/tests/workflow-qa-fixes.test.js +0 -377
  222. package/dist/tests/workflows/indexer-rejection.test.js +0 -213
  223. /package/dist/{src/commands → commands}/completions.js +0 -0
  224. /package/dist/{src/commands → commands}/curate.js +0 -0
  225. /package/dist/{src/commands → commands}/distill.js +0 -0
  226. /package/dist/{src/commands → commands}/events.js +0 -0
  227. /package/dist/{src/commands → commands}/info.js +0 -0
  228. /package/dist/{src/commands → commands}/init.js +0 -0
  229. /package/dist/{src/commands → commands}/install-audit.js +0 -0
  230. /package/dist/{src/commands → commands}/installed-stashes.js +0 -0
  231. /package/dist/{src/commands → commands}/migration-help.js +0 -0
  232. /package/dist/{src/commands → commands}/proposal.js +0 -0
  233. /package/dist/{src/commands → commands}/propose.js +0 -0
  234. /package/dist/{src/commands → commands}/reflect.js +0 -0
  235. /package/dist/{src/commands → commands}/remember.js +0 -0
  236. /package/dist/{src/commands → commands}/self-update.js +0 -0
  237. /package/dist/{src/commands → commands}/source-clone.js +0 -0
  238. /package/dist/{src/commands → commands}/vault.js +0 -0
  239. /package/dist/{src/core → core}/asset-ref.js +0 -0
  240. /package/dist/{src/core → core}/asset-registry.js +0 -0
  241. /package/dist/{src/core → core}/asset-spec.js +0 -0
  242. /package/dist/{src/core → core}/common.js +0 -0
  243. /package/dist/{src/core → core}/errors.js +0 -0
  244. /package/dist/{src/core → core}/events.js +0 -0
  245. /package/dist/{src/core → core}/frontmatter.js +0 -0
  246. /package/dist/{src/core → core}/lesson-lint.js +0 -0
  247. /package/dist/{src/core → core}/markdown.js +0 -0
  248. /package/dist/{src/core → core}/paths.js +0 -0
  249. /package/dist/{src/core → core}/proposals.js +0 -0
  250. /package/dist/{src/core → core}/warn.js +0 -0
  251. /package/dist/{src/core → core}/write-source.js +0 -0
  252. /package/dist/{src/indexer → indexer}/db.js +0 -0
  253. /package/dist/{src/indexer → indexer}/file-context.js +0 -0
  254. /package/dist/{src/indexer → indexer}/graph-boost.js +0 -0
  255. /package/dist/{src/indexer → indexer}/graph-extraction.js +0 -0
  256. /package/dist/{src/indexer → indexer}/manifest.js +0 -0
  257. /package/dist/{src/indexer → indexer}/memory-inference.js +0 -0
  258. /package/dist/{src/indexer → indexer}/metadata.js +0 -0
  259. /package/dist/{src/indexer → indexer}/search-fields.js +0 -0
  260. /package/dist/{src/indexer → indexer}/semantic-status.js +0 -0
  261. /package/dist/{src/indexer → indexer}/usage-events.js +0 -0
  262. /package/dist/{src/indexer → indexer}/walker.js +0 -0
  263. /package/dist/{src/integrations → integrations}/agent/config.js +0 -0
  264. /package/dist/{src/integrations → integrations}/agent/detect.js +0 -0
  265. /package/dist/{src/integrations → integrations}/agent/index.js +0 -0
  266. /package/dist/{src/integrations → integrations}/agent/prompts.js +0 -0
  267. /package/dist/{src/integrations → integrations}/lockfile.js +0 -0
  268. /package/dist/{src/llm → llm}/client.js +0 -0
  269. /package/dist/{src/llm → llm}/embedder.js +0 -0
  270. /package/dist/{src/llm → llm}/embedders/cache.js +0 -0
  271. /package/dist/{src/llm → llm}/embedders/local.js +0 -0
  272. /package/dist/{src/llm → llm}/embedders/types.js +0 -0
  273. /package/dist/{src/llm → llm}/feature-gate.js +0 -0
  274. /package/dist/{src/llm → llm}/graph-extract.js +0 -0
  275. /package/dist/{src/llm → llm}/index-passes.js +0 -0
  276. /package/dist/{src/llm → llm}/memory-infer.js +0 -0
  277. /package/dist/{src/llm → llm}/metadata-enhance.js +0 -0
  278. /package/dist/{src/output → output}/context.js +0 -0
  279. /package/dist/{src/registry → registry}/create-provider-registry.js +0 -0
  280. /package/dist/{src/registry → registry}/factory.js +0 -0
  281. /package/dist/{src/registry → registry}/origin-resolve.js +0 -0
  282. /package/dist/{src/registry → registry}/providers/index.js +0 -0
  283. /package/dist/{src/registry → registry}/providers/skills-sh.js +0 -0
  284. /package/dist/{src/registry → registry}/providers/types.js +0 -0
  285. /package/dist/{src/registry → registry}/resolve.js +0 -0
  286. /package/dist/{src/registry → registry}/types.js +0 -0
  287. /package/dist/{src/setup → setup}/detect.js +0 -0
  288. /package/dist/{src/setup → setup}/ripgrep-install.js +0 -0
  289. /package/dist/{src/setup → setup}/ripgrep-resolve.js +0 -0
  290. /package/dist/{src/setup → setup}/steps.js +0 -0
  291. /package/dist/{src/sources → sources}/include.js +0 -0
  292. /package/dist/{src/sources → sources}/provider-factory.js +0 -0
  293. /package/dist/{src/sources → sources}/provider.js +0 -0
  294. /package/dist/{src/sources → sources}/providers/filesystem.js +0 -0
  295. /package/dist/{src/sources → sources}/providers/git.js +0 -0
  296. /package/dist/{src/sources → sources}/providers/index.js +0 -0
  297. /package/dist/{src/sources → sources}/providers/install-types.js +0 -0
  298. /package/dist/{src/sources → sources}/providers/npm.js +0 -0
  299. /package/dist/{src/sources → sources}/providers/provider-utils.js +0 -0
  300. /package/dist/{src/sources → sources}/providers/sync-from-ref.js +0 -0
  301. /package/dist/{src/sources → sources}/providers/tar-utils.js +0 -0
  302. /package/dist/{src/sources → sources}/providers/website.js +0 -0
  303. /package/dist/{src/sources → sources}/resolve.js +0 -0
  304. /package/dist/{src/sources → sources}/types.js +0 -0
  305. /package/dist/{src/templates → templates}/wiki-templates.js +0 -0
  306. /package/dist/{src/version.js → version.js} +0 -0
  307. /package/dist/{src/workflows → workflows}/authoring.js +0 -0
  308. /package/dist/{src/workflows → workflows}/cli.js +0 -0
  309. /package/dist/{src/workflows → workflows}/db.js +0 -0
  310. /package/dist/{src/workflows → workflows}/document-cache.js +0 -0
  311. /package/dist/{src/workflows → workflows}/parser.js +0 -0
  312. /package/dist/{src/workflows → workflows}/renderer.js +0 -0
  313. /package/dist/{src/workflows → workflows}/schema.js +0 -0
  314. /package/dist/{src/workflows → workflows}/validator.js +0 -0
@@ -1,456 +0,0 @@
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
- });
@@ -1,125 +0,0 @@
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
- });