akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.5

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 (346) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/lint/base-linter.js +19 -5
  53. package/dist/commands/proposal/drain.js +251 -644
  54. package/dist/commands/proposal/proposal-cli.js +3 -18
  55. package/dist/commands/proposal/proposal-types.js +20 -41
  56. package/dist/commands/proposal/proposal.js +1 -2
  57. package/dist/commands/proposal/propose.js +134 -160
  58. package/dist/commands/proposal/repository.js +502 -1487
  59. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  60. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  61. package/dist/commands/proposal/validators/proposals.js +13 -89
  62. package/dist/commands/read/curate.js +63 -413
  63. package/dist/commands/read/search-cli.js +16 -33
  64. package/dist/commands/read/search.js +17 -23
  65. package/dist/commands/read/show.js +2 -13
  66. package/dist/commands/sources/bundle-cli.js +25 -2
  67. package/dist/commands/sources/bundle-config-ops.js +4 -0
  68. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  69. package/dist/commands/sources/info.js +2 -11
  70. package/dist/commands/sources/installed-stashes.js +197 -746
  71. package/dist/commands/sources/schema-repair.js +98 -129
  72. package/dist/commands/sources/source-add.js +62 -12
  73. package/dist/commands/sources/source-manage.js +9 -2
  74. package/dist/commands/sources/stash-cli.js +1 -1
  75. package/dist/commands/tasks/explain.js +10 -13
  76. package/dist/commands/tasks/tasks-cli.js +9 -8
  77. package/dist/commands/tasks/tasks.js +326 -930
  78. package/dist/commands/tasks/validate.js +42 -21
  79. package/dist/commands/workflow/plan.js +22 -29
  80. package/dist/commands/workflow-cli.js +4 -4
  81. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  82. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  83. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  84. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  85. package/dist/core/adapter/execution-source.js +17 -29
  86. package/dist/core/asset/asset-placement.js +4 -13
  87. package/dist/core/asset/resolve-ref.js +1 -1
  88. package/dist/core/bundle-id.js +42 -5
  89. package/dist/core/bundle-rename.js +291 -0
  90. package/dist/core/config/config-io.js +1 -2
  91. package/dist/core/config/config-schema.js +1 -33
  92. package/dist/core/config/config-walker.js +1 -1
  93. package/dist/core/config/config.js +163 -68
  94. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  95. package/dist/core/config/schema/embedding.js +20 -5
  96. package/dist/core/config/schema/engines.js +5 -0
  97. package/dist/core/config/schema/execution.js +1 -1
  98. package/dist/core/config/schema/experimental.js +1 -1
  99. package/dist/core/config/schema/improve-processes.js +21 -95
  100. package/dist/core/config/schema/improve.js +4 -42
  101. package/dist/core/config/schema/scheduler.js +12 -12
  102. package/dist/core/config/schema/search.js +6 -22
  103. package/dist/core/env-secret-ref.js +0 -1
  104. package/dist/core/errors.js +8 -9
  105. package/dist/core/file-lock.js +76 -173
  106. package/dist/core/logs-db.js +2 -2
  107. package/dist/core/paths.js +0 -27
  108. package/dist/core/redaction.js +109 -2
  109. package/dist/core/run-lock.js +2 -5
  110. package/dist/core/spawn-env.js +1 -1
  111. package/dist/core/state/migrations.js +108 -61
  112. package/dist/core/state-db-scope.js +2 -4
  113. package/dist/core/state-db.js +126 -692
  114. package/dist/core/type-presentation.js +1 -9
  115. package/dist/core/write-source.js +293 -1012
  116. package/dist/execution/input-contract.js +1 -1
  117. package/dist/execution/resolved-request.js +135 -689
  118. package/dist/execution/source.js +63 -257
  119. package/dist/execution/target-ref.js +1 -1
  120. package/dist/indexer/bundle-identity-guard.js +2 -2
  121. package/dist/indexer/db/graph-db.js +106 -46
  122. package/dist/indexer/ensure-index.js +44 -85
  123. package/dist/indexer/graph/graph-extraction.js +340 -562
  124. package/dist/indexer/graph/graph-related.js +130 -0
  125. package/dist/indexer/index-rebuild-lock.js +3 -11
  126. package/dist/indexer/index-writer-lock.js +8 -17
  127. package/dist/indexer/index-written-assets.js +139 -151
  128. package/dist/indexer/indexer.js +524 -846
  129. package/dist/indexer/materialize-embeddings.js +60 -397
  130. package/dist/indexer/passes/memory-inference.js +81 -90
  131. package/dist/indexer/passes/metadata.js +132 -200
  132. package/dist/indexer/read-preflight.js +0 -7
  133. package/dist/indexer/scan/doc-to-entry.js +1 -3
  134. package/dist/indexer/scan/drain-dir.js +1 -1
  135. package/dist/indexer/search/db-search.js +181 -590
  136. package/dist/indexer/search/fts-query.js +30 -41
  137. package/dist/indexer/search/ranking.js +28 -154
  138. package/dist/indexer/search/search-attribution.js +12 -32
  139. package/dist/indexer/search/search-fields.js +11 -15
  140. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  141. package/dist/indexer/search/search-source.js +1 -4
  142. package/dist/indexer/usage/usage-events.js +2 -7
  143. package/dist/integrations/agent/engine-fallback.js +23 -40
  144. package/dist/integrations/agent/engine-resolution.js +93 -183
  145. package/dist/integrations/agent/execution.js +507 -0
  146. package/dist/integrations/agent/model-map.js +28 -156
  147. package/dist/integrations/agent/request-lowering.js +66 -141
  148. package/dist/integrations/agent/runner-dispatch.js +143 -321
  149. package/dist/integrations/agent/runner.js +54 -14
  150. package/dist/integrations/lockfile.js +53 -101
  151. package/dist/llm/embedders/deterministic.js +2 -3
  152. package/dist/llm/embedders/profile.js +71 -0
  153. package/dist/llm/embedders/remote.js +10 -15
  154. package/dist/llm/graph-extract.js +3 -12
  155. package/dist/llm/index-passes.js +3 -5
  156. package/dist/llm/memory-infer.js +1 -2
  157. package/dist/llm/metadata-enhance.js +1 -2
  158. package/dist/llm/structured-call.js +5 -24
  159. package/dist/output/generic-render.js +23 -11
  160. package/dist/output/html-render.js +13 -10
  161. package/dist/output/render-registry.js +3 -32
  162. package/dist/output/shapes/helpers.js +2 -34
  163. package/dist/output/shapes/passthrough.js +1 -9
  164. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  165. package/dist/output/text/command-format.js +60 -23
  166. package/dist/output/text/helpers.js +1 -1
  167. package/dist/output/text/migrate.js +5 -14
  168. package/dist/output/text/proposal-format.js +1 -2
  169. package/dist/output/text/workflow-format.js +0 -32
  170. package/dist/output/text.js +2 -0
  171. package/dist/registry/factory.js +4 -19
  172. package/dist/registry/network.js +66 -220
  173. package/dist/registry/providers/index.js +0 -2
  174. package/dist/registry/providers/skills-sh.js +3 -14
  175. package/dist/registry/providers/static-index.js +24 -26
  176. package/dist/registry/resolve.js +55 -131
  177. package/dist/scripts/akm-migrate-node.js +43940 -93320
  178. package/dist/scripts/akm-migrate.js +43700 -93078
  179. package/dist/setup/registry-stash-loader.js +4 -13
  180. package/dist/setup/semantic-assets.js +3 -44
  181. package/dist/setup/setup.js +1 -1
  182. package/dist/setup/steps/tasks.js +25 -15
  183. package/dist/sources/provider-factory.js +17 -18
  184. package/dist/sources/providers/filesystem.js +2 -3
  185. package/dist/sources/providers/git-install.js +7 -1
  186. package/dist/sources/providers/git-provider.js +0 -3
  187. package/dist/sources/providers/git-stash.js +0 -17
  188. package/dist/sources/providers/npm.js +2 -4
  189. package/dist/sources/providers/provider-utils.js +5 -10
  190. package/dist/sources/providers/website.js +0 -2
  191. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  192. package/dist/sources/website-url.js +2 -2
  193. package/dist/storage/database.js +9 -35
  194. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  195. package/dist/storage/repositories/index-connection.js +34 -70
  196. package/dist/storage/repositories/index-entries-repository.js +69 -111
  197. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  198. package/dist/storage/repositories/index-entry-schema.js +83 -269
  199. package/dist/storage/repositories/index-fts-repository.js +86 -256
  200. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  201. package/dist/storage/repositories/index-meta-repository.js +6 -4
  202. package/dist/storage/repositories/index-schema.js +192 -220
  203. package/dist/storage/repositories/index-utility-repository.js +8 -29
  204. package/dist/storage/repositories/index-vec-repository.js +133 -414
  205. package/dist/storage/repositories/outcome-repository.js +2 -1
  206. package/dist/storage/repositories/proposals-repository.js +35 -0
  207. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  208. package/dist/storage/repositories/task-history-repository.js +26 -4
  209. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  210. package/dist/storage/sqlite-migrations.js +136 -0
  211. package/dist/storage/sqlite-pragmas.js +11 -9
  212. package/dist/storage/sqlite-transaction.js +170 -0
  213. package/dist/storage/state-db-integrity.js +34 -27
  214. package/dist/tasks/activation-config.js +134 -62
  215. package/dist/tasks/backends/cron.js +129 -277
  216. package/dist/tasks/backends/exec-utils.js +2 -5
  217. package/dist/tasks/backends/launchd.js +125 -745
  218. package/dist/tasks/backends/schtasks.js +101 -620
  219. package/dist/tasks/prepare/prepare-support.js +5 -15
  220. package/dist/tasks/prepare/prepare.js +0 -2
  221. package/dist/tasks/resolve-akm-bin.js +20 -79
  222. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  223. package/dist/tasks/scheduler-binding.js +18 -238
  224. package/dist/tasks/scheduler-invocation.js +52 -52
  225. package/dist/tasks/scheduler-lock.js +53 -0
  226. package/dist/tasks/scheduler-sync.js +361 -751
  227. package/dist/tasks/source/parse-task-source.js +160 -10
  228. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  229. package/dist/tasks/source/task-to-v4.js +2 -2
  230. package/dist/workflows/authoring/authoring.js +3 -12
  231. package/dist/workflows/compile.js +211 -0
  232. package/dist/workflows/concurrency-policy.js +13 -74
  233. package/dist/workflows/exec/child-invocation.js +3 -17
  234. package/dist/workflows/exec/child-workflow.js +32 -141
  235. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  236. package/dist/workflows/exec/environment.js +98 -0
  237. package/dist/workflows/exec/exec-unit.js +33 -140
  238. package/dist/workflows/exec/frozen-judge.js +7 -59
  239. package/dist/workflows/exec/native-executor.js +82 -341
  240. package/dist/workflows/exec/param-secrets.js +29 -47
  241. package/dist/workflows/exec/run-workflow.js +154 -387
  242. package/dist/workflows/exec/scheduler.js +9 -36
  243. package/dist/workflows/exec/step-work.js +127 -430
  244. package/dist/workflows/exec/unit-dispatch.js +11 -63
  245. package/dist/workflows/exec/unit-writer.js +8 -52
  246. package/dist/workflows/exec/worktree.js +39 -273
  247. package/dist/workflows/freeze/child-output-references.js +4 -15
  248. package/dist/workflows/freeze/environment.js +99 -92
  249. package/dist/workflows/freeze/freeze.js +172 -0
  250. package/dist/workflows/freeze/step-values.js +19 -21
  251. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  252. package/dist/workflows/freeze/targets/command.js +10 -33
  253. package/dist/workflows/freeze/targets/script.js +5 -12
  254. package/dist/workflows/freeze/targets/shell.js +3 -6
  255. package/dist/workflows/freeze/targets/task.js +25 -80
  256. package/dist/workflows/freeze/task-bindings.js +20 -67
  257. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  258. package/dist/workflows/ir/params.js +6 -51
  259. package/dist/workflows/ir/plan-hash.js +2 -34
  260. package/dist/workflows/parser.js +140 -43
  261. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  262. package/dist/workflows/renderer.js +36 -69
  263. package/dist/workflows/resource-limits.js +12 -120
  264. package/dist/workflows/runtime/agent-identity.js +8 -40
  265. package/dist/workflows/runtime/run-outputs.js +3 -6
  266. package/dist/workflows/runtime/run-plan.js +316 -0
  267. package/dist/workflows/runtime/runs.js +48 -200
  268. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  269. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  270. package/dist/workflows/validate-summary.js +2 -7
  271. package/docs/integration/bundling-akm.md +49 -42
  272. package/docs/migration/README.md +1 -0
  273. package/docs/migration/release-notes/0.9.17.md +41 -0
  274. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  275. package/docs/reference/cli.md +182 -125
  276. package/docs/reference/configuration.md +49 -56
  277. package/docs/reference/data-and-telemetry.md +19 -20
  278. package/docs/reference/tasks.md +86 -38
  279. package/docs/reference/workflow-schema.md +14 -18
  280. package/docs/reference/workflows.md +6 -9
  281. package/package.json +1 -1
  282. package/schemas/akm-config.json +87 -406
  283. package/dist/commands/health/advisories.js +0 -150
  284. package/dist/commands/health/metrics.js +0 -329
  285. package/dist/commands/health/surfaces.js +0 -102
  286. package/dist/commands/improve/anti-collapse.js +0 -83
  287. package/dist/commands/improve/collapse-detector.js +0 -432
  288. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  289. package/dist/commands/improve/consolidate/merge.js +0 -146
  290. package/dist/commands/improve/distill/promote-memory.js +0 -329
  291. package/dist/commands/improve/distill/quality-gate.js +0 -500
  292. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  293. package/dist/commands/improve/proposal-envelope.js +0 -31
  294. package/dist/commands/improve/run-context.js +0 -123
  295. package/dist/commands/improve/shared.js +0 -21
  296. package/dist/commands/improve/source-identity.js +0 -28
  297. package/dist/commands/improve/triage.js +0 -96
  298. package/dist/commands/proposal/drain-policies.js +0 -151
  299. package/dist/commands/sources/update-transaction.js +0 -220
  300. package/dist/core/action-contributors.js +0 -28
  301. package/dist/core/config/config-version-shim.js +0 -101
  302. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  303. package/dist/core/fs-txn.js +0 -405
  304. package/dist/core/lexical-score.js +0 -25
  305. package/dist/core/maintenance-barrier.js +0 -167
  306. package/dist/execution/executable-identity.js +0 -105
  307. package/dist/execution/guarded-source.js +0 -441
  308. package/dist/indexer/graph/graph-boost.js +0 -427
  309. package/dist/indexer/graph/graph-dedup.js +0 -95
  310. package/dist/indexer/search/name-match.js +0 -35
  311. package/dist/indexer/search/ranking-contributors.js +0 -515
  312. package/dist/indexer/walk/project-context.js +0 -192
  313. package/dist/integrations/agent/execution-cascade.js +0 -566
  314. package/dist/integrations/agent/execution-definitions.js +0 -202
  315. package/dist/integrations/agent/execution-lowering.js +0 -841
  316. package/dist/integrations/agent/execution-preparation.js +0 -98
  317. package/dist/integrations/agent/inline-execution.js +0 -74
  318. package/dist/registry/create-provider-registry.js +0 -29
  319. package/dist/registry/pinned-request-helper.js +0 -247
  320. package/dist/registry/pinned-transport.js +0 -717
  321. package/dist/sources/providers/index.js +0 -14
  322. package/dist/storage/engines/sqlite-migrations.js +0 -271
  323. package/dist/storage/repositories/canaries-repository.js +0 -107
  324. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  325. package/dist/storage/repositories/registry-cache.js +0 -113
  326. package/dist/tasks/scheduler-sync-preview.js +0 -52
  327. package/dist/workflows/freeze/resolve-steps.js +0 -86
  328. package/dist/workflows/freeze/source-freeze.js +0 -64
  329. package/dist/workflows/ir/compile.js +0 -321
  330. package/dist/workflows/ir/environment-v4.js +0 -330
  331. package/dist/workflows/ir/freeze-v4.js +0 -153
  332. package/dist/workflows/ir/schema-v4.js +0 -745
  333. package/dist/workflows/ir/schema.js +0 -354
  334. package/dist/workflows/program/schema.js +0 -78
  335. package/dist/workflows/runtime/checkin.js +0 -57
  336. package/dist/workflows/runtime/plan-classifier.js +0 -196
  337. package/dist/workflows/runtime/unit-checkin.js +0 -45
  338. package/dist/workflows/runtime/unit-phases.js +0 -20
  339. package/dist/workflows/schema.js +0 -4
  340. package/dist/workflows/source-ir/compile.js +0 -200
  341. package/dist/workflows/source-ir/program.js +0 -50
  342. package/dist/workflows/source-ir/result.js +0 -26
  343. package/dist/workflows/source-ir/schema.js +0 -786
  344. package/dist/workflows/source-ir/triggers.js +0 -79
  345. package/dist/workflows/source-ir/uses.js +0 -40
  346. package/dist/workflows/validator.js +0 -60
@@ -2,288 +2,141 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * Reflect noise gate (#580) — deterministic detection of no-op and
6
- * cosmetic-only reflect candidates.
7
- *
8
- * Manual review of the proposal queue (June 2026) found that 2 of 5 pending
9
- * reflect proposals had literally EMPTY diffs and 2 more were cosmetic-only
10
- * (YAML description re-folding, code-fence language hints, whitespace
11
- * reflow / unwrapping of hard-wrapped prose). Each wasted an LLM call AND a
12
- * review-queue slot. This module classifies a candidate edit against the
13
- * current asset BEFORE `createProposal()` so reflect can short-circuit.
14
- *
15
- * Classification is pure text comparison — no LLM involved:
16
- *
17
- * - `"noop"` — byte-identical after trailing-whitespace normalization.
18
- * - `"cosmetic"` — identical after a conservative cosmetic normalization
19
- * of both sides (frontmatter compared as parsed YAML so
20
- * folded/literal scalar re-folding cancels out; markdown
21
- * body compared with hard-wrapped prose unwrapped,
22
- * whitespace runs collapsed, and code-fence language
23
- * hints stripped).
24
- * - `"substantive"` — everything else, including every case the
25
- * normalizer is unsure about.
26
- *
27
- * The normalizer is deliberately conservative: a false NEGATIVE (letting a
28
- * cosmetic proposal through) costs one review-queue slot; a false POSITIVE
29
- * (suppressing a real fix) loses work. When in doubt, classify as
30
- * substantive. Concretely:
31
- *
32
- * - Fenced code block contents are compared verbatim (whitespace inside
33
- * code is significant).
34
- * - Indented (4-space / tab) code lines are compared verbatim.
35
- * - Headings, tables, and thematic breaks never absorb the following
36
- * line, so `# Title\nprose` vs `# Title prose` stays substantive.
37
- * - Prose only unwraps into the previous line when that line is itself
38
- * plain prose or a list/blockquote item (CommonMark lazy continuation).
5
+ * The reflect noise gate (#580): classify a candidate edit against the current
6
+ * asset before it becomes a proposal, by text comparison alone. Empty and
7
+ * cosmetic-only edits (YAML re-folding, fence language hints, re-wrapped prose)
8
+ * each cost an LLM call and a review slot. The gate is conservative — letting a
9
+ * cosmetic edit through costs a review, suppressing a real fix loses work — so
10
+ * anything uncertain is `substantive`: code (fenced or indented) compares
11
+ * verbatim, and headings, tables and breaks never absorb the next line.
39
12
  */
40
13
  import { parse as yamlParse } from "yaml";
41
14
  /**
42
- * Classify a reflect candidate against the current asset content.
43
- *
44
- * `"noop"` and `"cosmetic"` candidates must never become proposals; only
45
- * `"substantive"` changes proceed to `createProposal()`.
46
- *
47
- * `"low-value"` is a tier between `"cosmetic"` and `"substantive"` (#639):
48
- * a small prose rewrite that changes few content-words outside fenced code,
49
- * without introducing new headings, list items, or code lines. The caller
50
- * decides whether to act on it (config-gated, DEFAULT OFF).
15
+ * `noop` (identical up to trailing whitespace) and `cosmetic` (identical after
16
+ * normalizing frontmatter as parsed YAML and prose as unwrapped text) never
17
+ * become proposals. `low-value` (#639) is a small prose rewrite — few changed
18
+ * words, nothing structural; the caller decides (off by default).
51
19
  */
52
20
  export function classifyReflectChange(sourceContent, candidateContent) {
53
- if (normalizeTrailingWhitespace(sourceContent) === normalizeTrailingWhitespace(candidateContent)) {
21
+ if (normalizeTrailingWhitespace(sourceContent) === normalizeTrailingWhitespace(candidateContent))
54
22
  return "noop";
55
- }
56
23
  try {
57
- if (cosmeticNormalForm(sourceContent) === cosmeticNormalForm(candidateContent)) {
24
+ if (cosmeticNormalForm(sourceContent) === cosmeticNormalForm(candidateContent))
58
25
  return "cosmetic";
59
- }
60
26
  }
61
27
  catch {
62
- // Conservative: a normalizer error means we cannot prove the change is
63
- // cosmetic — let the proposal through.
28
+ // unprovable → substantive
64
29
  }
65
- // Low-value tier (#639): deterministic semantic-value-floor check.
66
30
  try {
67
- if (isLowValueChange(sourceContent, candidateContent)) {
31
+ if (isLowValueChange(sourceContent, candidateContent))
68
32
  return "low-value";
69
- }
70
33
  }
71
34
  catch {
72
- // Conservative: treat any classifier error as substantive.
35
+ // unprovable → substantive
73
36
  }
74
37
  return "substantive";
75
38
  }
76
- // ---------------------------------------------------------------------------
77
- // Low-value tier (#639) helpers
78
- // ---------------------------------------------------------------------------
79
- /**
80
- * Threshold: the maximum number of net changed content-words in eligible prose
81
- * lines for a change to be classified as `"low-value"`. Conservative/low to
82
- * minimise false positives (substantive changes suppressed).
83
- */
39
+ /** A `---` frontmatter block and the rest (`fmText: null` when there is none). */
40
+ export function splitFrontmatter(raw) {
41
+ const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
42
+ return m ? { fmText: m[1] ?? "", body: m[2] ?? "" } : { fmText: null, body: raw };
43
+ }
44
+ /** At most this many changed words (and at least 2 — one word may be a flag flip) is low-value. */
84
45
  const LOW_VALUE_TOKEN_THRESHOLD = 4;
85
- /**
86
- * Negation / flag-correction words that make a line exempt from low-value
87
- * classification even when the token-change count is below the threshold.
88
- * A line that introduces or removes one of these words carries enough semantic
89
- * weight to be treated as substantive.
90
- */
46
+ /** A change to any of these is never low-value. */
91
47
  const NEGATION_WORDS = new Set(["never", "not", "no", "don't", "avoid", "cannot", "can't"]);
92
- /**
93
- * Pattern for lines that contain decision / outcome markers. Changed tokens on
94
- * these lines are EXEMPT from the low-value token count (they are semantically
95
- * significant regardless of token volume).
96
- */
48
+ /** Lines recording a decision or outcome are always significant; their words are not counted. */
97
49
  const DECISION_MARKER_RE = /\b(decision|outcome)\b/i;
98
- /**
99
- * Pattern for lines that start a new structural element (ATX heading, list
100
- * item, thematic break). A candidate that ADDS such lines is not low-value.
101
- */
102
50
  const STRUCTURAL_LINE_RE = /^\s*(#{1,6}\s|[-*+]\s|\d{1,9}[.)]\s|---+|===+|\|\s)/;
103
- /**
104
- * Determine whether the change from `source` to `candidate` qualifies as
105
- * `"low-value"`: a small prose rewrite with few changed content-words outside
106
- * fenced code / frontmatter / decision markers, without adding structural lines.
107
- *
108
- * Conservative / false-negative-biased: returns `false` (= substantive) when
109
- * any exemption condition is met or when the parser is unsure.
110
- */
111
51
  function isLowValueChange(source, candidate) {
112
- const srcParsed = parseLowValueSections(source);
113
- const cndParsed = parseLowValueSections(candidate);
114
- // Any change inside a code fence → substantive (code is always significant).
115
- if (srcParsed.codeFences.join("\n") !== cndParsed.codeFences.join("\n")) {
116
- return false;
117
- }
118
- // Any change in frontmatter → substantive (description / when_to_use are semantic).
119
- if (srcParsed.frontmatter !== cndParsed.frontmatter) {
52
+ const src = parseLowValueSections(source);
53
+ const cnd = parseLowValueSections(candidate);
54
+ // Code and frontmatter changes are always substantive.
55
+ if (src.codeFences.join("\n") !== cnd.codeFences.join("\n") || src.frontmatter !== cnd.frontmatter)
120
56
  return false;
121
- }
122
- // Check prose body lines for low-value criteria.
123
- const srcLines = srcParsed.proseLines;
124
- const cndLines = cndParsed.proseLines;
125
- // Build a word-diff over prose lines. Align by line index (conservative: if
126
- // line counts differ significantly, call it substantive).
127
- if (Math.abs(srcLines.length - cndLines.length) > 2) {
57
+ if (Math.abs(src.proseLines.length - cnd.proseLines.length) > 2)
128
58
  return false;
129
- }
130
- const maxLen = Math.max(srcLines.length, cndLines.length);
131
59
  let changedTokens = 0;
132
- let hasNewStructuralLine = false;
133
- for (let i = 0; i < maxLen; i++) {
134
- const srcLine = srcLines[i] ?? "";
135
- const cndLine = cndLines[i] ?? "";
136
- if (srcLine === cndLine)
137
- continue;
138
- // Lines with decision/outcome markers are exempt — skip their token diff.
139
- if (DECISION_MARKER_RE.test(srcLine) || DECISION_MARKER_RE.test(cndLine)) {
60
+ for (let i = 0; i < Math.max(src.proseLines.length, cnd.proseLines.length); i++) {
61
+ const srcLine = src.proseLines[i] ?? "";
62
+ const cndLine = cnd.proseLines[i] ?? "";
63
+ if (srcLine === cndLine || DECISION_MARKER_RE.test(srcLine) || DECISION_MARKER_RE.test(cndLine))
140
64
  continue;
141
- }
142
- // Any structural line (heading, list item, thematic break) changed → not
143
- // low-value. Structural changes affect document organization and are
144
- // inherently significant regardless of token count.
145
- if (STRUCTURAL_LINE_RE.test(cndLine) || STRUCTURAL_LINE_RE.test(srcLine)) {
146
- hasNewStructuralLine = true;
147
- break;
148
- }
149
- // Tokenise both lines into lowercase words.
65
+ if (STRUCTURAL_LINE_RE.test(cndLine) || STRUCTURAL_LINE_RE.test(srcLine))
66
+ return false;
150
67
  const srcTokens = tokenize(srcLine);
151
68
  const cndTokens = tokenize(cndLine);
152
- // Check for negation / flag-correction words: if the set of negation words
153
- // differs between versions → the change is semantically significant (exempt).
154
- const srcNegSet = new Set(srcTokens.filter((t) => NEGATION_WORDS.has(t)));
155
- const cndNegSet = new Set(cndTokens.filter((t) => NEGATION_WORDS.has(t)));
156
- const negSetsEqual = srcNegSet.size === cndNegSet.size && [...srcNegSet].every((t) => cndNegSet.has(t));
157
- if (!negSetsEqual) {
69
+ const srcNeg = new Set(srcTokens.filter((t) => NEGATION_WORDS.has(t)));
70
+ const cndNeg = new Set(cndTokens.filter((t) => NEGATION_WORDS.has(t)));
71
+ if (srcNeg.size !== cndNeg.size || [...srcNeg].some((t) => !cndNeg.has(t)))
158
72
  return false;
159
- }
160
- // Count changed tokens: words unique to each side (symmetric difference).
161
- // Counting max(added, removed) approximates "number of substitutions" so
162
- // that "is→runs" + "default→defaults" counts as 2, not 4.
73
+ // max(added, removed) approximates substitutions: "is→runs" counts once.
163
74
  const srcSet = new Set(srcTokens);
164
75
  const cndSet = new Set(cndTokens);
165
- const addedTokens = cndTokens.filter((t) => !srcSet.has(t));
166
- const removedTokens = srcTokens.filter((t) => !cndSet.has(t));
167
- changedTokens += Math.max(addedTokens.length, removedTokens.length);
76
+ changedTokens += Math.max(cndTokens.filter((t) => !srcSet.has(t)).length, srcTokens.filter((t) => !cndSet.has(t)).length);
168
77
  }
169
- if (hasNewStructuralLine)
170
- return false;
171
- // Must have at least 2 changed tokens to qualify as low-value:
172
- // - 0 changed tokens → noop/cosmetic (handled upstream, never reaches here)
173
- // - 1 changed token → single-word substitution; too small to be reliably
174
- // "low-value" vs a semantically significant flag flip → treat as substantive
175
- // - 2..threshold-1 → genuine prose micro-rewrite → low-value
176
78
  return changedTokens >= 2 && changedTokens < LOW_VALUE_TOKEN_THRESHOLD;
177
79
  }
178
- /** Split a document into its frontmatter text, code-fence blocks, and prose lines. */
80
+ /** Frontmatter text, fenced code blocks, and prose lines (an unclosed fence counts as prose). */
179
81
  function parseLowValueSections(text) {
180
- const normalized = normalizeTrailingWhitespace(text);
181
- // Split off frontmatter.
182
- const fmMatch = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
183
- const frontmatter = fmMatch ? (fmMatch[1] ?? "") : "";
184
- const body = fmMatch ? (fmMatch[2] ?? normalized) : normalized;
185
- const lines = body.split("\n");
82
+ const { fmText, body } = splitFrontmatter(normalizeTrailingWhitespace(text));
186
83
  const codeFences = [];
187
84
  const proseLines = [];
188
- let inFence = false;
189
- let currentFence = [];
190
- let fenceMarker = "";
191
- for (const line of lines) {
192
- if (inFence) {
193
- const closeMatch = line.match(/^(\s{0,3})(`{3,}|~{3,})\s*$/);
194
- if (closeMatch?.[2]?.startsWith(fenceMarker)) {
195
- inFence = false;
196
- codeFences.push(currentFence.join("\n"));
197
- currentFence = [];
198
- fenceMarker = "";
199
- }
200
- else {
201
- currentFence.push(line);
202
- }
203
- }
204
- else {
205
- const openMatch = line.match(/^(\s{0,3})(`{3,}|~{3,})/);
206
- if (openMatch) {
207
- inFence = true;
208
- fenceMarker = openMatch[2] ?? "```";
209
- currentFence = [];
85
+ let fence;
86
+ for (const line of body.split("\n")) {
87
+ if (fence) {
88
+ const close = line.match(/^(\s{0,3})(`{3,}|~{3,})\s*$/);
89
+ if (close?.[2]?.startsWith(fence.marker)) {
90
+ codeFences.push(fence.lines.join("\n"));
91
+ fence = undefined;
210
92
  }
211
93
  else {
212
- proseLines.push(line);
94
+ fence.lines.push(line);
213
95
  }
96
+ continue;
214
97
  }
215
- }
216
- // Unclosed fence: treat as prose (conservative).
217
- if (inFence && currentFence.length > 0) {
218
- proseLines.push(...currentFence);
219
- }
220
- return { frontmatter, codeFences, proseLines };
98
+ const open = line.match(/^(\s{0,3})(`{3,}|~{3,})/);
99
+ if (open)
100
+ fence = { marker: open[2] ?? "```", lines: [] };
101
+ else
102
+ proseLines.push(line);
103
+ }
104
+ if (fence)
105
+ proseLines.push(...fence.lines);
106
+ return { frontmatter: fmText ?? "", codeFences, proseLines };
221
107
  }
222
- /** Split a prose line into lowercase word tokens, filtering punctuation. */
223
108
  function tokenize(line) {
224
- return (line
109
+ return line
225
110
  .toLowerCase()
226
- // Normalize the curly apostrophe (U+2019) to a straight one so negation words
227
- // like "don't"/"can't" match regardless of which glyph the content uses.
228
- .replace(/’/g, "'")
111
+ .replace(/’/g, "'") // curly apostrophes still match "don't"
229
112
  .replace(/[^a-z0-9'-]/g, " ")
230
113
  .split(/\s+/)
231
- .filter((t) => t.length > 0));
114
+ .filter((t) => t.length > 0);
232
115
  }
233
- /**
234
- * Trailing-whitespace normalization used for the `"noop"` check: CRLF → LF,
235
- * strip trailing spaces/tabs per line, drop trailing newlines.
236
- */
116
+ /** CRLF → LF, no trailing spaces per line, no trailing newlines. */
237
117
  export function normalizeTrailingWhitespace(text) {
238
118
  return text
239
119
  .replace(/\r\n/g, "\n")
240
120
  .replace(/[ \t]+$/gm, "")
241
121
  .replace(/\n+$/, "");
242
122
  }
243
- /**
244
- * Compute the cosmetic normal form of an asset: canonicalized frontmatter
245
- * (parsed YAML, keys sorted) + normalized markdown body, joined with a
246
- * separator that cannot occur in either part.
247
- *
248
- * Two assets with equal normal forms differ only cosmetically.
249
- */
123
+ /** Canonical frontmatter (parsed YAML, keys sorted) plus the normalized body; equal forms differ only cosmetically. */
250
124
  export function cosmeticNormalForm(text) {
251
- const { fmText, body } = splitFrontmatterText(normalizeTrailingWhitespace(text));
252
- const fmCanonical = fmText === null ? "" : canonicalizeYamlText(fmText);
253
- return `${fmCanonical}\u0000${normalizeMarkdownBody(body)}`;
254
- }
255
- /**
256
- * Split a normalized blob into `{ fmText, body }`. Returns `fmText: null`
257
- * when the blob does not start with a `---` frontmatter block. Mirrors the
258
- * splitter in `reflect.ts` (kept local: this module must stay dependency-light
259
- * and pure so the normalizer is trivially unit-testable).
260
- */
261
- function splitFrontmatterText(raw) {
262
- const m = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
263
- if (!m)
264
- return { fmText: null, body: raw };
265
- return { fmText: m[1] ?? "", body: m[2] ?? "" };
266
- }
267
- /**
268
- * Canonicalize a YAML frontmatter block: parse and re-serialize as JSON with
269
- * recursively sorted object keys. Folded (`>`) and literal (`|`) scalars,
270
- * quoting style, and key order all cancel out; the parsed VALUES are what we
271
- * compare. Unparsable YAML falls back to the raw text (trailing-stripped) so
272
- * any real edit still registers as a difference.
273
- */
274
- function canonicalizeYamlText(fmText) {
275
- try {
276
- return stableStringify(yamlParse(fmText));
277
- }
278
- catch {
279
- return fmText;
125
+ const { fmText, body } = splitFrontmatter(normalizeTrailingWhitespace(text));
126
+ let fmCanonical = "";
127
+ if (fmText !== null) {
128
+ try {
129
+ fmCanonical = stableStringify(yamlParse(fmText));
130
+ }
131
+ catch {
132
+ fmCanonical = fmText; // unparsable: any real edit still registers
133
+ }
280
134
  }
135
+ return `${fmCanonical}\u0000${normalizeMarkdownBody(body)}`;
281
136
  }
282
- /** JSON.stringify with recursively sorted object keys (arrays keep order). */
283
137
  function stableStringify(value) {
284
- if (Array.isArray(value)) {
138
+ if (Array.isArray(value))
285
139
  return `[${value.map(stableStringify).join(",")}]`;
286
- }
287
140
  if (value !== null && typeof value === "object") {
288
141
  const entries = Object.entries(value)
289
142
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
@@ -292,100 +145,71 @@ function stableStringify(value) {
292
145
  }
293
146
  return JSON.stringify(value);
294
147
  }
295
- /** Opening or closing fence: optional indent + ``` / ~~~ + optional info string. */
296
148
  const FENCE_LINE = /^(\s{0,3})(`{3,}|~{3,})(.*)$/;
297
- /**
298
- * Lines that terminate a logical line and never absorb a following prose
299
- * line: ATX headings, setext underlines, thematic breaks, table rows.
300
- */
149
+ /** Headings, setext underlines, thematic breaks and table rows never absorb the next line. */
301
150
  const TERMINAL_LINE = /^\s{0,3}(#{1,6}(\s|$)|=+\s*$|(-\s*){3,}$|(\*\s*){3,}$|(_\s*){3,}$|\|)/;
302
- /** List items and blockquotes — start a logical line but allow lazy continuation. */
151
+ /** List items and blockquotes allow lazy continuation. */
303
152
  const CONTINUABLE_LINE = /^\s*([-*+]\s|\d{1,9}[.)]\s|>)/;
304
- /** Indented code block line (outside any list context — treated verbatim). */
305
153
  const INDENTED_CODE_LINE = /^(\t| {4})/;
306
154
  /**
307
- * Normalize a markdown body for cosmetic comparison:
308
- *
309
- * - code-fence language hints stripped from opening fences; fenced
310
- * contents kept verbatim
311
- * - indented code lines kept verbatim
312
- * - hard-wrapped prose unwrapped into the preceding prose / list /
313
- * blockquote line (single space join)
314
- * - runs of spaces/tabs inside prose collapsed to one space
315
- * - runs of blank lines collapsed to one; leading/trailing blanks dropped
155
+ * A markdown body for cosmetic comparison: fence language hints dropped (code
156
+ * kept verbatim), indented code verbatim, hard-wrapped prose joined into the
157
+ * preceding prose/list/quote line, inner whitespace runs collapsed, blank runs
158
+ * collapsed and trimmed.
316
159
  */
317
160
  export function normalizeMarkdownBody(body) {
318
- const lines = normalizeTrailingWhitespace(body).split("\n");
319
161
  const logical = [];
320
162
  let lastKind = "blank";
321
163
  let fenceMarker = null;
322
- for (const line of lines) {
164
+ const push = (line, kind) => {
165
+ logical.push(line);
166
+ lastKind = kind;
167
+ };
168
+ for (const line of normalizeTrailingWhitespace(body).split("\n")) {
323
169
  if (fenceMarker !== null) {
324
170
  const close = line.match(FENCE_LINE);
325
171
  if (close?.[2]?.startsWith(fenceMarker) && close[3]?.trim() === "") {
326
172
  fenceMarker = null;
327
- logical.push(line.trim());
328
- lastKind = "terminal";
173
+ push(line.trim(), "terminal");
329
174
  }
330
175
  else {
331
- logical.push(line); // code content: compare verbatim
332
- lastKind = "verbatim";
176
+ push(line, "verbatim");
333
177
  }
334
178
  continue;
335
179
  }
336
180
  const fence = line.match(FENCE_LINE);
337
181
  if (fence) {
338
182
  fenceMarker = fence[2] ?? "```";
339
- // Strip the info string (language hint) — ```js and ``` compare equal.
340
- logical.push(fenceMarker);
341
- lastKind = "terminal";
342
- continue;
183
+ push(fenceMarker, "terminal");
343
184
  }
344
- if (line.trim() === "") {
345
- if (lastKind !== "blank") {
346
- logical.push("");
347
- lastKind = "blank";
348
- }
349
- continue;
185
+ else if (line.trim() === "") {
186
+ if (lastKind !== "blank")
187
+ push("", "blank");
350
188
  }
351
- if (INDENTED_CODE_LINE.test(line)) {
352
- logical.push(line);
353
- lastKind = "verbatim";
354
- continue;
189
+ else if (INDENTED_CODE_LINE.test(line)) {
190
+ push(line, "verbatim");
355
191
  }
356
- if (TERMINAL_LINE.test(line)) {
357
- logical.push(collapseInnerWhitespace(line));
358
- lastKind = "terminal";
359
- continue;
192
+ else if (TERMINAL_LINE.test(line)) {
193
+ push(collapseInnerWhitespace(line), "terminal");
360
194
  }
361
- if (CONTINUABLE_LINE.test(line)) {
362
- logical.push(collapseInnerWhitespace(line));
363
- lastKind = "continuable";
364
- continue;
195
+ else if (CONTINUABLE_LINE.test(line)) {
196
+ push(collapseInnerWhitespace(line), "continuable");
365
197
  }
366
- // Plain prose: unwrap into the previous logical line when it can lazily
367
- // continue (prose or list/blockquote); otherwise start a new logical line.
368
- const collapsed = collapseInnerWhitespace(line.trim());
369
- if (lastKind === "prose" || lastKind === "continuable") {
370
- // lastKind stays joinable (prose continuation keeps absorbing).
371
- logical[logical.length - 1] = `${logical[logical.length - 1]} ${collapsed}`;
198
+ else if (lastKind === "prose" || lastKind === "continuable") {
199
+ logical[logical.length - 1] = `${logical[logical.length - 1]} ${collapseInnerWhitespace(line.trim())}`;
372
200
  }
373
201
  else {
374
- logical.push(collapsed);
375
- lastKind = "prose";
202
+ push(collapseInnerWhitespace(line.trim()), "prose");
376
203
  }
377
204
  }
378
- // Drop leading/trailing blank logical lines.
379
205
  while (logical[0] === "")
380
206
  logical.shift();
381
207
  while (logical[logical.length - 1] === "")
382
208
  logical.pop();
383
209
  return logical.join("\n");
384
210
  }
385
- /** Collapse internal space/tab runs to a single space, preserving leading indent. */
211
+ /** Collapse inner space/tab runs, keeping the leading indent. */
386
212
  function collapseInnerWhitespace(line) {
387
213
  const m = line.match(/^([ \t]*)([\s\S]*)$/);
388
- const indent = m?.[1] ?? "";
389
- const rest = m?.[2] ?? "";
390
- return indent + rest.replace(/[ \t]+/g, " ");
214
+ return (m?.[1] ?? "") + (m?.[2] ?? "").replace(/[ \t]+/g, " ");
391
215
  }