akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -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/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. 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
  }