@chemx/starter-kit 26.10.19-530 → 26.10.26-386

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 (297) hide show
  1. package/AGENTS.md +37 -18
  2. package/README.md +21 -12
  3. package/cli/apply-edits.js +4 -1
  4. package/cli/args-unsplit-hint.js +33 -0
  5. package/cli/audit/audit-plain.js +9 -0
  6. package/cli/audit/audit-summary.js +2 -2
  7. package/cli/audit/coverage.js +27 -3
  8. package/cli/audit/inconclusive.js +36 -0
  9. package/cli/audit/line-budgets.js +9 -1
  10. package/cli/audit/ratchet.js +45 -6
  11. package/cli/audit/rule-overrides.js +18 -4
  12. package/cli/audit/rule-revisions.js +23 -2
  13. package/cli/audit/rules-registry.js +104 -0
  14. package/cli/audit/rules.js +51 -11
  15. package/cli/audit/status-file.js +1 -1
  16. package/cli/audit/suppressions.js +47 -14
  17. package/cli/audit-scan.js +31 -8
  18. package/cli/build/detector.js +41 -0
  19. package/cli/build/executor.js +7 -3
  20. package/cli/build/jest-config-modules.js +76 -0
  21. package/cli/build/pnpm-store-path.js +60 -0
  22. package/cli/build.js +1 -1
  23. package/cli/check-report.js +52 -0
  24. package/cli/ci-checks.js +132 -0
  25. package/cli/ci-command.js +123 -0
  26. package/cli/ci-gh.js +38 -0
  27. package/cli/ci-protection.js +101 -0
  28. package/cli/cli-args.js +2 -0
  29. package/cli/cli-root-flags.js +17 -0
  30. package/cli/cli-root.js +246 -0
  31. package/cli/commands/cmd-audit.js +4 -1
  32. package/cli/commands/cmd-batch.js +4 -1
  33. package/cli/commands/cmd-check.js +12 -9
  34. package/cli/commands/cmd-mutate.js +193 -0
  35. package/cli/commands/cmd-router.js +43 -3
  36. package/cli/commands/cmd-run.js +125 -0
  37. package/cli/commands/cmd-scratch.js +70 -0
  38. package/cli/commands/cmd-worktree.js +214 -0
  39. package/cli/commands/unknown-flags.js +4 -2
  40. package/cli/commands/worktree-fixture.js +70 -0
  41. package/cli/commands-schema-badge.js +15 -0
  42. package/cli/commands-schema-ci.js +20 -0
  43. package/cli/commands-schema-commit.js +7 -4
  44. package/cli/commands-schema-edit.js +5 -5
  45. package/cli/commands-schema-host.js +5 -4
  46. package/cli/commands-schema-ops.js +11 -21
  47. package/cli/commands-schema-search.js +17 -7
  48. package/cli/commands-schema-team.js +87 -0
  49. package/cli/commands-schema-verify.js +15 -12
  50. package/cli/commands-schema-workspace.js +48 -0
  51. package/cli/commands-schema-wrappers.js +15 -10
  52. package/cli/commands-schema.js +3 -1
  53. package/cli/commit/commit-args.js +11 -7
  54. package/cli/commit/commit-facts.js +26 -10
  55. package/cli/commit/commit-git.js +21 -0
  56. package/cli/commit/commit-message.js +23 -8
  57. package/cli/commit/commit-record.js +6 -16
  58. package/cli/commit/commit-run.js +15 -6
  59. package/cli/commit/commit-validate.js +27 -3
  60. package/cli/config/index.js +2 -0
  61. package/cli/docs-check/changelog-fold.js +98 -0
  62. package/cli/docs-check/changelog-fragments.js +79 -0
  63. package/cli/docs-check/changelog-run.js +80 -0
  64. package/cli/docs-check/command-tree.js +5 -2
  65. package/cli/docs-check/resolve.js +4 -3
  66. package/cli/doctor/check-env.js +27 -1
  67. package/cli/doctor/check-kit.js +106 -0
  68. package/cli/doctor/check-mcp.js +19 -13
  69. package/cli/doctor/doctor-cli.js +20 -10
  70. package/cli/doctor/jest-deps.js +91 -0
  71. package/cli/doctor/mcp-groups.js +86 -0
  72. package/cli/doctor/proc-scan.js +22 -4
  73. package/cli/edit-guardrails.js +20 -10
  74. package/cli/git-file-listing.js +7 -4
  75. package/cli/help.js +7 -2
  76. package/cli/hooks/claude-pre-tool.js +68 -20
  77. package/cli/hooks/declared-identity.js +136 -0
  78. package/cli/hooks/dispatch-identity.js +49 -4
  79. package/cli/hooks/guard-allow.js +84 -0
  80. package/cli/hooks/guard-args.js +13 -0
  81. package/cli/hooks/guard-cases.js +3 -1
  82. package/cli/hooks/guard-config.js +5 -2
  83. package/cli/hooks/guard-find.js +38 -0
  84. package/cli/hooks/guard-invocation.js +1 -1
  85. package/cli/hooks/guard-paths.js +14 -6
  86. package/cli/hooks/guard-refine.js +131 -0
  87. package/cli/hooks/guard-rules-nudge.js +42 -3
  88. package/cli/hooks/guard-rules-reads.js +3 -8
  89. package/cli/hooks/guard-rules-shell.js +6 -16
  90. package/cli/hooks/guard-rules.js +11 -12
  91. package/cli/hooks/guard-shapes.js +104 -0
  92. package/cli/hooks/guard-test-runners.js +47 -0
  93. package/cli/hooks/native-edit-lock.js +31 -10
  94. package/cli/hooks/run-hook.js +10 -0
  95. package/cli/hooks/session-identity.js +12 -24
  96. package/cli/host-protocol.js +12 -9
  97. package/cli/index-admission.js +1 -1
  98. package/cli/index-freshness.js +2 -0
  99. package/cli/index-seed-parent.js +71 -0
  100. package/cli/index-seed.js +185 -0
  101. package/cli/index.js +20 -1
  102. package/cli/job-line.js +7 -8
  103. package/cli/job-record.js +67 -0
  104. package/cli/lattice-directives.js +10 -0
  105. package/cli/license.js +28 -3
  106. package/cli/lint-error.js +36 -0
  107. package/cli/lint-micro.js +91 -0
  108. package/cli/lint-report.js +77 -0
  109. package/cli/lint-scope.js +67 -0
  110. package/cli/main.js +15 -0
  111. package/cli/mcp/help.js +2 -2
  112. package/cli/mcp/manifests-team.js +25 -5
  113. package/cli/mcp/manifests.js +6 -4
  114. package/cli/mcp/tools-audit.js +7 -3
  115. package/cli/mcp/tools-q.js +20 -33
  116. package/cli/mcp/tools-team-feed.js +12 -23
  117. package/cli/mcp/tools-team-task-extras.js +41 -0
  118. package/cli/mcp/tools-team-tasks.js +54 -15
  119. package/cli/mutate-edit.js +190 -0
  120. package/cli/patch-block-blame.js +61 -0
  121. package/cli/patcher-cli.js +36 -8
  122. package/cli/patcher.js +22 -3
  123. package/cli/php/php-analyzer.js +241 -0
  124. package/cli/php/php-callables.js +170 -0
  125. package/cli/php/php-catch.js +88 -0
  126. package/cli/php/php-comments.js +58 -0
  127. package/cli/php/php-control-flow.js +345 -0
  128. package/cli/php/php-dangerous.js +138 -0
  129. package/cli/php/php-facts-vocab.js +52 -0
  130. package/cli/php/php-facts.js +399 -0
  131. package/cli/php/php-logging.js +101 -0
  132. package/cli/php/php-metrics.js +131 -0
  133. package/cli/php/php-names.js +151 -0
  134. package/cli/php/php-outline.js +42 -0
  135. package/cli/php/php-parse.js +119 -0
  136. package/cli/php/php-registrations.js +176 -0
  137. package/cli/php/php-symbols.js +209 -0
  138. package/cli/php/php-taint.js +447 -0
  139. package/cli/php/php-walk.js +189 -0
  140. package/cli/php/wp-auth-filters.js +170 -0
  141. package/cli/php/wp-detect.js +197 -0
  142. package/cli/php/wp-fail-open.js +69 -0
  143. package/cli/php/wp-index-files.js +219 -0
  144. package/cli/php/wp-nonce.js +90 -0
  145. package/cli/php/wp-output.js +138 -0
  146. package/cli/php/wp-project-index.js +333 -0
  147. package/cli/php/wp-registration.js +90 -0
  148. package/cli/php/wp-request-array.js +84 -0
  149. package/cli/php/wp-request-flow.js +432 -0
  150. package/cli/php/wp-rest-endpoints.js +338 -0
  151. package/cli/php/wp-route-bounds.js +173 -0
  152. package/cli/php/wp-route-rules.js +150 -0
  153. package/cli/php/wp-route-shadow.js +69 -0
  154. package/cli/php/wp-sql-classify.js +418 -0
  155. package/cli/php/wp-sql-lookup.js +245 -0
  156. package/cli/php/wp-sql-members.js +135 -0
  157. package/cli/php/wp-sql.js +105 -0
  158. package/cli/reader-logic.js +2 -0
  159. package/cli/reader-outline-kind.js +15 -0
  160. package/cli/reader.js +9 -2
  161. package/cli/scratch-dir.js +60 -0
  162. package/cli/search-args.js +3 -2
  163. package/cli/search-ast.js +4 -0
  164. package/cli/search-commands-literal.js +91 -99
  165. package/cli/search-commands.js +8 -43
  166. package/cli/search-literal-argv.js +34 -0
  167. package/cli/search-literal-files.js +23 -9
  168. package/cli/search-literal-view.js +55 -0
  169. package/cli/search-literal.js +24 -13
  170. package/cli/search-matched-text.js +22 -0
  171. package/cli/search-output.js +4 -1
  172. package/cli/search-q-fallback.js +13 -0
  173. package/cli/search-query-view.js +11 -7
  174. package/cli/search-regex-guard.js +86 -0
  175. package/cli/search-replace-blocks.js +36 -7
  176. package/cli/search-scan.js +12 -1
  177. package/cli/search-scope-check.js +28 -0
  178. package/cli/search-sync.js +6 -1
  179. package/cli/search.js +36 -46
  180. package/cli/status-command.js +18 -11
  181. package/cli/team/agent-identity.js +33 -0
  182. package/cli/team/audit-run-identity.js +8 -11
  183. package/cli/team/audit-run-invocations.js +2 -1
  184. package/cli/team/board-claim-record.js +34 -0
  185. package/cli/team/board-claims.js +125 -0
  186. package/cli/team/board-notice.js +47 -0
  187. package/cli/team/board-resolve.js +144 -0
  188. package/cli/team/claim-ignore-deps.js +15 -3
  189. package/cli/team/cli-exit-fixture.js +24 -0
  190. package/cli/team/coordination-busy.js +20 -1
  191. package/cli/team/help/benchmark.js +6 -0
  192. package/cli/team/help/brief.js +6 -0
  193. package/cli/team/help/card-rows.js +20 -0
  194. package/cli/team/help/dm.js +6 -0
  195. package/cli/team/help/feed.js +7 -0
  196. package/cli/team/help/inbox.js +6 -0
  197. package/cli/team/help/lock.js +50 -0
  198. package/cli/team/help/post.js +6 -0
  199. package/cli/team/help/route.js +6 -0
  200. package/cli/team/help/shared.js +7 -0
  201. package/cli/team/help/status.js +6 -0
  202. package/cli/team/help/task-actions.js +22 -0
  203. package/cli/team/help/task-options.js +44 -0
  204. package/cli/team/help/task.js +15 -0
  205. package/cli/team/help/team-card.js +18 -0
  206. package/cli/team/help/tokens.js +6 -0
  207. package/cli/team/help/train.js +6 -0
  208. package/cli/team/help/triage.js +6 -0
  209. package/cli/team/help/unlock.js +6 -0
  210. package/cli/team/identity-classification.js +205 -0
  211. package/cli/team/lease-activity.js +40 -18
  212. package/cli/team/lease-claim-roots.js +36 -0
  213. package/cli/team/lease-grace.js +103 -0
  214. package/cli/team/lock-hints.js +14 -0
  215. package/cli/team/lock-lines.js +57 -0
  216. package/cli/team/lock-targets.js +97 -0
  217. package/cli/team/task-add-from-file.js +157 -0
  218. package/cli/team/task-brief-store.js +89 -0
  219. package/cli/team/task-brief-view.js +95 -0
  220. package/cli/team/task-claim-age.js +103 -0
  221. package/cli/team/task-completion-output.js +3 -0
  222. package/cli/team/task-detail-sections.js +13 -1
  223. package/cli/team/task-file-rows.js +255 -0
  224. package/cli/team/task-kind.js +28 -0
  225. package/cli/team/task-list-view.js +32 -11
  226. package/cli/team/task-ref.js +16 -0
  227. package/cli/team/task-update-status.js +32 -0
  228. package/cli/team/task-yaml-lite.js +228 -0
  229. package/cli/team/team-board-target.js +52 -0
  230. package/cli/team/team-commands-board.js +17 -24
  231. package/cli/team/team-commands-lock-views.js +17 -5
  232. package/cli/team/team-commands-lock.js +103 -85
  233. package/cli/team/team-commands-repo.js +4 -1
  234. package/cli/team/team-commands-task.js +97 -22
  235. package/cli/team/team-commands-tokens.js +2 -1
  236. package/cli/team/team-commands-vds.js +10 -2
  237. package/cli/team/team-commands.js +57 -25
  238. package/cli/team/team-db-agents.js +5 -1
  239. package/cli/team/team-db-feed.js +31 -1
  240. package/cli/team/team-db-locks.js +3 -0
  241. package/cli/team/team-db-task-helpers.js +9 -2
  242. package/cli/team/team-db-tasks.js +12 -5
  243. package/cli/team/team-dependents-gate.js +46 -0
  244. package/cli/team/team-dispatch-prompts.js +1 -1
  245. package/cli/team/team-feed-view.js +71 -0
  246. package/cli/team/team-flag-values.js +56 -0
  247. package/cli/team/team-flags.js +39 -83
  248. package/cli/team/team-format.js +72 -135
  249. package/cli/team/team-help-golden.json +311 -0
  250. package/cli/team/team-schema.js +9 -0
  251. package/cli/team/team-status-view.js +50 -0
  252. package/cli/team/team-task-guard.js +5 -3
  253. package/cli/team/team-task-handoff.js +17 -4
  254. package/cli/team/team-tokens-epic.js +190 -0
  255. package/cli/team/team-triage-verify.js +42 -4
  256. package/cli/team/team-triage.js +6 -2
  257. package/cli/team/team-view-flags.js +39 -0
  258. package/cli/team/usage-render.js +2 -2
  259. package/cli/team/write-lock-guard.js +6 -3
  260. package/cli/terminal.js +16 -0
  261. package/cli/test-args.js +46 -0
  262. package/cli/test-audit.js +23 -10
  263. package/cli/test-changes.js +26 -3
  264. package/cli/test-command.js +7 -2
  265. package/cli/test-graph-names.js +58 -0
  266. package/cli/test-graph-open.js +2 -1
  267. package/cli/test-graph.js +31 -55
  268. package/cli/test-kit-fixture.js +43 -0
  269. package/cli/test-lanes.js +10 -5
  270. package/cli/test-meta.js +35 -0
  271. package/cli/test-open-ok.js +55 -0
  272. package/cli/test-output.js +19 -1
  273. package/cli/test-project-runner.js +119 -0
  274. package/cli/test-report.js +56 -10
  275. package/cli/test-run-size.js +22 -0
  276. package/cli/test-run.js +19 -4
  277. package/cli/test-scope.js +13 -5
  278. package/cli/test-select.js +70 -12
  279. package/cli/test-slots.js +30 -19
  280. package/cli/test-workers.js +4 -2
  281. package/cli/verify-audit-step.js +45 -0
  282. package/cli/verify-changed.js +5 -1
  283. package/cli/verify-harness.js +83 -0
  284. package/cli/verify-helpers.js +15 -1
  285. package/cli/verify-lint.js +112 -131
  286. package/cli/verify-php-lint.js +102 -0
  287. package/cli/verify-report.js +40 -0
  288. package/cli/verify-steps.js +1 -1
  289. package/cli/verify.js +28 -48
  290. package/cli/wait-command.js +65 -10
  291. package/cli/wait-notice.js +27 -0
  292. package/cli/wait-post.js +45 -0
  293. package/cli/worktree-base.js +49 -0
  294. package/cli/worktree-git.js +94 -0
  295. package/cli/worktree-index.js +42 -0
  296. package/cli/worktree-modules.js +75 -0
  297. package/package.json +7 -3
package/AGENTS.md CHANGED
@@ -8,17 +8,31 @@
8
8
  ## ⚡ Quick Agent Reflex Table (Token-Bounded Commands)
9
9
  | Standard Reflex | `chemx` Equivalent | Token & Architecture Advantage |
10
10
  | :--- | :--- | :--- |
11
- | `grep -rn "pattern" .` | `chemx q -g "pattern" --bare` or `chemx q -g "pattern" -C 3 --bare` | Fixed-string search matching rg token-for-token (57 chars / 14 tokens per hit); add `-C <N>` to inspect surrounding code in a single turn without a second read call. |
11
+ | `grep -rn "pattern" .` | `chemx q -g "pattern" --bare` or `chemx q -g "pattern" -C 3 --bare` | Fixed-string search matching rg token-for-token (57 chars / 14 tokens per hit); add `-C <N>` to inspect surrounding code in a single turn without a second read call. `a\|b` is literal text: add `--regex` for alternation (RE2 subset). `-l` prints the matching files once, `-o --unique` prints the distinct matched text (a `--gs-[a-z-]+` token list in one call), paths after the pattern scope it (`--glob <path>` is `--dir`; `--max <N>` is `-n`), dot-directories such as `.github` and `.claude` are searched (only git-ignored paths are left out; `--hidden` adds them), and `chemx q <text>` with no symbol hit runs this search for you. See [docs/search.md](docs/search.md). |
12
12
  | `sed -n 'N,Mp' <file>` | `chemx read <file>:N-M --bare` | Verbatim slice matching sed byte-for-byte; 0 header, box or line number tokens. |
13
13
  | `git diff` | `chemx d` | Zero-context (`-U0`), auto-collapses to `--stat` if > 80 lines, micro-syncs index. |
14
+ | Editing `docs/CHANGELOG.md` for your change | write `docs/changelog.d/<ticket>.md` (first line `### Fixed`, `### Added` or a title with the ticket id, then bullets); `chemx changelog --check` validates, `chemx changelog` folds the fragments into Unreleased and deletes them | One file per change, so parallel tickets never edit the same lines. Never edit `docs/CHANGELOG.md` directly. See [docs/changelog.d/README.md](docs/changelog.d/README.md). |
14
15
  | `git log` | `chemx log -n 5` | One-line commit history (5 commits measured about 140 tokens on 2026-10-09, roughly 28 per commit). |
15
16
  | `find . -name "*.vue"` | `chemx f "*.vue"` | Strictly filters by `.gitignore` and ignored dirs. |
16
17
  | `cat package.json` | `chemx p -s` / `chemx p <script>` | Single script or dep extraction (`p -s` listing 15 scripts measured about 240 tokens on 2026-10-09). |
17
18
  | `cat <data.json>` | `chemx j <file.json>` | Structural schema shape only; collapses repeating arrays. |
18
19
  | `cat <file>` / `head <file>` | `chemx read <file> --outline` | AST signatures only (outline of `cli/reader.js` measured about 300 tokens against about 4,800 for the file, 2026-10-09; other targets in `benchmarks/README.md`). |
19
20
  | `npm test` / `tsc` | `chemx test` / `chemx verify` | Prints a summary on success and the failing diffs on failure. |
21
+ | `eslint <paths>` / `npm run lint` | `chemx lint <paths> [--fix]` | ESLint grouped by file, plus the `chemx check` hazards of those paths in the same pass (inline booleans, dispatch switches), so there is no second pass. With no ESLint config or package.json (a WordPress plugin dir) it says so in one line and runs `chemx check` on the files instead. |
22
+ | `node tests/run-tests.mjs` (a repo's own harness runner, e.g. the WordPress plugins) | `chemx test` / `chemx test tests/x.test.mjs` | One file through a generated copy of the runner, failures only and one summary line, one run at a time on the machine (shared database). `.chemxrc` `guard.allow` (or its alias `test.runners`) lists a raw runner the guard should leave alone. |
20
23
  | `cat .chemx/history.json` | `chemx audit --feed=scopes` / MCP `audit_feed` | Recorded audit results as flat rows (per scope, per run or per pillar) without re-running the audit; `--json` for every row. |
21
24
  | Auditing each package by hand | `chemx audit --each=submodules` | One scoped audit per submodule or workspace package, then a one-line-per-package summary. |
25
+ | `cd ../other-repo && <command>` / `git -C ../other-repo log` | `chemx <command> --root=../other-repo` (any command: `log`, `d`, `status`, `q -g`, `read`, `team ...`; or `CHEMX_PROJECT_ROOT=<dir>`) | Runs as if started in that project, from any cwd; the shell never changes directory. `chemx read /abs/path/in/another/chemx/project` picks that project's index too. |
26
+ | `cd ../other-repo && chemx team task show 12` | `chemx team task show ../other-repo#12` or `--board=../other-repo` | A task number belongs to a board; this names it. A `team task list`/`show` footer says when you hold a claim on a different board. |
27
+ | `gh pr checks <n>`, `gh pr view <n> --json mergeStateStatus`, `sleep 120; gh pr checks <n>` | `chemx ci <n>` or `chemx ci <n> --wait` | One line per check (state, duration, name) and one merge-state line; exit 0 all passed, 1 a check failed, 3 pending or no checks reported yet, 2 could not read. `--wait` polls every 30 s and prints only what changed. `chemx ci --protection` warns (exit 1) when the default branch requires no status checks, because `gh pr merge --auto` then merges at once. See [docs/ci.md](docs/ci.md). |
28
+ | Adding up task costs by hand | `chemx team tokens --epic <id>` | Prompt, completion and cached tokens, cost and wall time over an epic's children (recursively), one line each and a total; a child that never recorded usage is a dash and counted unknown, not zero. See [docs/team-tokens.md](docs/team-tokens.md). |
29
+ | A shell loop of `team task add` to file an epic and its children | `chemx task add --from tasks.yaml` (`--dry-run` first) | Rows with `ref`, `parent_ref` and `deps` by ref, one transaction, one `#<id> added` line per row and a `refs:` map; any wrong row writes nothing. `chemx task` is `chemx team task`. |
30
+ | A 7 KB design contract in `--desc`, or in a `.md` file agents cat | `chemx team brief <id> --file design.md` (read: `task show <id> --full`) | The brief is stored on the task apart from its description; `task show` prints only its length. |
31
+ | `sleep`/poll loops for a peer's report | `chemx wait --post-matching "K9 merged" --from=@sonnet-k9` | Resolves when a feed post (any case, no lock events) matches; exit 0 / 2 on timeout. |
32
+ | `chemx test ... &` then a `sleep` or `kill -0` loop until it ends | `chemx test ... & chemx wait --job=$!` | Resolves when that chemx process has ended and exits with the job's own code (0 pass, 1 fail, 3 inconclusive); 2 on timeout. See [docs/test-lanes.md](docs/test-lanes.md). |
33
+ | A one-off script in a scratchpad other agents also write to (`/tmp/one.mjs`) | `chemx scratch one.mjs` (the directory: `chemx scratch`) | Prints `<project>/.chemx/scratch/<handle>/one.mjs` and makes the folder: one directory per handle, git-ignored, so another agent's `one.mjs` never replaces yours. See [docs/agent-workspace.md](docs/agent-workspace.md). |
34
+ | `git worktree add ../w -b fix/x origin/main` then a hand-made `node_modules` symlink; later `git worktree remove` and `git branch -d` | `chemx worktree add <path> -b <branch> [--link-modules\|--install]` / `chemx worktree remove <path>` | Branches from the remote default branch, gives the worktree a real `node_modules` of symlinks into the main checkout's (so an install in it never writes into the main checkout) or installs, and prints the path first plus the reminder that `chemx team` runs from the main checkout or with `--board`. `remove` deletes the branch when it is merged, squash merges included, into the default branch or the branch it was cut from (recorded at `add`), and refuses an unmerged one without `--force`. See [docs/agent-workspace.md](docs/agent-workspace.md). |
35
+ | `cp f f.bak; sed -i ... f; <test>; cp -i f.bak f` (a mutation check) | `chemx mutate f --apply 's/a/b/' --then "<test>"` | Applies a sed substitution or a diff file, runs the command like `chemx run`, always restores the original byte for byte (a copy sits in your scratch directory; `--keep` retains it) and exits with the command's code (125 = chemx stopped, e.g. nothing changed or a lease is held). See [docs/agent-workspace.md](docs/agent-workspace.md). |
22
36
  | Multiple CLI actions | `chemx do "<cmd1>" "<cmd2>"` | Executes sequentially in a single warm Node process. |
23
37
  | Multiple MCP tool calls | `chemx({ commands: [...] })` | Executes multiple sub-operations in a single agent turn. |
24
38
 
@@ -112,7 +126,7 @@ Use same-name shorthand across all languages; eliminate redundant `key: key` dup
112
126
  ### L. Database-First Swarm Coordination & The Zero-Markdown-Monolith Directive
113
127
  - **Database as the Master Source of Truth**: The multi-agent orchestrator and subagents MUST record all milestones, task assignments, status changes, file locks, and architectural contracts directly in `.chemx/index.db` (`agent_tasks`, `file_lock_queue`, `agent_feed`) rather than creating monolithic, multi-thousand-token markdown specification files.
114
128
  - **Prohibition on Monolithic Project Markdown Parsing**: Subagents MUST NOT load giant monolithic `PROJECT.md` or status markdown files into context. Subagents query only their assigned task slice and dependencies via `pnpm chemx team task list --agent=@my-id` or MCP `chemx_team_task`.
115
- - **Leases and handoff**: A file lock is a 5-minute lease that any chemx command run as the holder extends and that lapses after 5 minutes without activity (`docs/team-locks.md`). The commit hook refuses staged files another handle leases live. `chemx team task handoff <id> @<to> --as=@<from>` passes a task on (assignee or task creator only); it keeps the task `in_progress`, records from/to/by in the feed, and does not move file locks, so the old holder releases and the new owner acquires.
129
+ - **Leases and handoff**: A file lock is a 5-minute lease that any chemx command run as the holder extends (from a git worktree too, once the holder has claimed a task) and that lapses after 5 minutes without activity; a lapse of at most 10 minutes is undone by the holder's next chemx command while the task is in_progress and nobody took or queued for the file. Wrap a long non-chemx command (a plugin's test runner, a script) as `chemx run -- <command>` and it renews your leases every minute while it runs (`docs/team-locks.md`). The commit hook refuses staged files another handle leases live. Claude's native `Edit`/`Write` are refused on a file another handle leases, but a lease held by a handle you named in an earlier chemx call (`--as`, `CHEMX_AGENT_ID`) this session is yours: name your handle on your lock and claim calls (`docs/hooks.md`, "Native edits and file locks"). `chemx team task handoff <id> @<to> --as=@<from>` passes a task on (assignee or task creator only); it keeps the task `in_progress`, records from/to/by in the feed, moves the board pointer to the new owner (their next `chemx commit` finds the task's board without `--board`), and does not move file locks, so the old holder releases and the new owner acquires. `chemx team lock a.js b.js c.js --as=@<you>` takes several paths in one call and answers one line per path (`granted until HH:MM`, `queued behind @x`, or `refused: <reason>`); an absolute path in a sibling repo works from any directory under the board root.
116
130
  - **Project Markdown as a Generated Read-Only Projection**: If a human-readable `PROJECT.md` is requested, it MUST be generated on-demand as a read-only projection compiled from the database state via `pnpm chemx team status` or export tools.
117
131
 
118
132
  ### M. The Master MCP Tool Protocol & Single-Permission Dispatch
@@ -151,6 +165,8 @@ Use same-name shorthand across all languages; eliminate redundant `key: key` dup
151
165
  // Preview first: dryRun returns the unified diff and writes nothing (CLI: --dry-run or -n)
152
166
  chemx({ action: 'patch', params: { path: 'src/...', target: 'oldCode', replacement: 'newCode', dryRun: true } });
153
167
  chemx({ action: 'patch', params: { path: 'src/...', target: 'oldCode', replacement: 'newCode' } });
168
+ // Several edits to one file go in ONE call (CLI: SEARCH/REPLACE blocks in a heredoc; API: blocks: [{ search, replace }]): all-or-nothing,
169
+ // and a block that does not match or trips a guard fails the call as "Block 2 of 3: ..." with the file untouched
154
170
  // Removing a top-level declaration (renames included) is refused unless named (CLI: --allow-remove=oldName)
155
171
  chemx({ action: 'patch', params: { path: 'src/...', target: 'oldName', replacement: 'newName', allowRemoved: ['oldName'] } });
156
172
  // write creates; replacing an existing file needs overwrite (CLI: --overwrite)
@@ -176,7 +192,7 @@ Use same-name shorthand across all languages; eliminate redundant `key: key` dup
176
192
  ### N. Database-First Navigation, Symbol Connections & The Zero-Native-File-Dump Directive
177
193
  - **Database Master Index First**: Before inspecting or modifying any file, AI agents MUST query the SQLite database (`.chemx/index.db`) or the AST Query Machine (`chemx({ action: 'team', params: { action: 'list' } })`, `chemx({ action: 'q', params: { query } })`) rather than exploring the filesystem with broad find or ripgrep commands.
178
194
  - **Targeted Symbol-Only Extraction**: AI agents MUST NEVER dump entire source files into context. When an agent needs to inspect a function, hook, interface, or class, it MUST request only that specific symbol: `chemx({ action: 'read', params: { path, symbol: '<name>' } })`.
179
- - **Read Window Auto-Outline**: Files longer than the chemx read window (100 lines by default), read without a target symbol or slice, automatically render an AST outline to prevent token exhaustion and host buffer spillovers (such as IDE `output.txt` dumps). The read window is a tool budget, not an architecture rule; file size is governed by 1.A.
195
+ - **Read Window Auto-Outline**: Files longer than the chemx read window (100 lines by default), read without a target symbol or slice, automatically render an AST outline to prevent token exhaustion and host buffer spillovers (such as IDE `output.txt` dumps). A file with nothing to outline (yaml, json, txt, shell, or Markdown without headings) reads as its first window of text with the next range named, not an empty outline. The read window is a tool budget, not an architecture rule; file size is governed by 1.A.
180
196
  - **Enrich Mode (Component Capsules Only)**: When an agent needs both exported signatures AND logic flow for a component capsule, use `enrich: true` with `outline: true`: one response card holds the outline and a compacted logic skeleton, optionally with a forward trace or reverse caller chain (`traceSymbol`, `backtraceSymbol`). The skeleton generator is tuned for components; on procedural modules (services, CLI code, utilities) enrich saves only a few percent over reading the whole file, so use plain `outline: true` or `symbol: '<name>'` there.
181
197
  - **Symbol Connection Graph Over Multi-File Dumps**: Rather than reading multiple files to understand imports and consumers, agents MUST request symbol connections: `chemx({ action: 'read', params: { path, symbol: '<name>', connections: true } })` or `chemx({ action: 'q', params: { query, connections: true } })`. This returns the definition, imported dependencies, and caller references. Size grows with the symbol: `q <symbol> --connections` measured about 140 tokens and `read --symbol --connections` about 2,400 for `readTokenOptimized` on 2026-10-09.
182
198
  - **Token-First File Inspection & Native Fallback**: AI agents should prioritize Chemical X AST readers (`chemx read --outline` or `chemx read --symbol`) to conserve tokens. Dumping raw files burns thousands of tokens and causes premature context exhaustion. If an agent requires exact raw verification or unformatted bytes, native tools (`view_file`, `grep`, `cat`) are permitted as an intentional fallback.
@@ -527,9 +543,10 @@ Raw inline `style="..."` attributes are strictly prohibited. Visual styling flow
527
543
  - Strictly prohibit executing verbose raw `npm test` or `pnpm test` in the shell: hundreds of passing test markers pollute context. If tests pass, agents get a short green summary; if tests fail, they get the failing test names, assertion messages, and diffs.
528
544
  - **Test loop, in order of cost** (details: `docs/test-lanes.md`):
529
545
  1. `chemx test --changed --depth=3`: only the specs within 3 import hops of your change. Fastest, and NOT proof: the report lists how many deeper specs were skipped.
530
- 2. `chemx test --changed`: every spec the import graph says your change can reach, fast lane only. Slow-lane specs that it reaches are named as not run.
531
- 3. `chemx test`: the whole fast lane. `chemx test --all` adds the slow lane (specs that spawn the CLI or MCP server, stress and timing specs); that is what CI's `npm test` runs. `chemx test --slow` runs only the slow lane.
532
- 4. `chemx test --profile [--all] [--top=<n>]`: per-spec-file wall time, slowest first, with the measuring method printed beside the numbers.
546
+ 2. `chemx test --changed`: every spec the import graph says your change can reach, plus the meta specs that scan the whole repo (`test-lanes.json` `meta`), fast lane only. On a clean tree it compares with the merge base of origin/main (or origin/master) and prints the base it chose. Slow-lane specs that it reaches are named as not run. A line `widened by cli/x.js (computed import without open-ok): +N specs` names a file whose computed `import()` put N specs into the run; declare a real entry point with `// chemx: open-ok` or an `openImports` rule in `test-lanes.json` instead of paying for it.
547
+ 3. `chemx test --changed --all`: the same affected set, both lanes. CI runs both lanes, so run this (not step 2) before you push.
548
+ 4. `chemx test`: the whole fast lane. `chemx test --all` adds the slow lane (specs that spawn the CLI or MCP server, stress and timing specs); that is what CI's `npm test` runs. `chemx test --slow` runs only the slow lane.
549
+ 5. `chemx test --profile [--all] [--top=<n>]`: per-spec-file wall time, slowest first, with the measuring method printed beside the numbers.
533
550
  - Lane membership lives in `test-lanes.json`, not in `package.json`. A new spec is fast unless the manifest (or a `*.slow.spec.js`, `*.perf.spec.js`, `*.e2e.spec.js` file name) says otherwise. A spec file that no test glob collects fails `cli/test-lanes-coverage.spec.js`.
534
551
  - A spec that asserts a wall-clock or CPU budget belongs in the slow lane (name it `*.perf.spec.js`), or its margin must hold on a machine running many agents.
535
552
 
@@ -571,7 +588,7 @@ Raw inline `style="..."` attributes are strictly prohibited. Visual styling flow
571
588
  ### A. Installed Hooks, Not Prose
572
589
  * Install the Claude Code integration with `chemx install-hooks --host=claude [--scope=local|project] [--dry-run]`. It adds the PreToolUse guard (`chemx hook claude-pre-tool`), the PostToolUse check (`claude-post-edit`), the SessionStart card and the statusline. The `.mcp.json` launch, the git pre-commit hook and the CI pin are opt-in: add `--write-mcp`, `--git-hooks` and `--pin-ci`; without them none of those files is written. Run `chemx doctor` when MCP answers look stale; `chemx doctor --fix` repairs hooks and the MCP launch only.
573
590
  * Alternatively install the Claude Code plugin (`plugins/claude-code`, marketplace `.claude-plugin/marketplace.json`): the same hooks, the chemical-x MCP server and a short chemx skill, resolving the kit from `CHEMX_KIT`, the project's node_modules or PATH. Use the plugin or install-hooks, not both, or every hook runs twice. The statusline comes only from install-hooks.
574
- * The guard tokenizes the shell command. Raw test/typecheck/lint/build runners, `git diff`/`git log` and reads of repo source files are routed through chemx. Native Read, Edit, Write and Grep on project text files are warned by default (`nativeFileTools` in `.chemxrc`, or `CHEMX_NATIVE_FILE_TOOLS`): `warn` names the chemx equivalent, `block` denies the call, `allow` is silent.
591
+ * The guard tokenizes the shell command. Raw test/typecheck/lint/build runners, `git diff`/`git log` and reads of repo source files are routed through chemx, but a call is denied only where the chemx call covers its exact shape: `git -C <dir> log`, `git log A..B`, `grep -c`/`-o` and similar get a nudge with the closest chemx form (`chemx log --root <dir>`, `chemx log -n 50 A..B`, `chemx q -g ... -o --unique`), and shapes chemx cannot do (`grep -v`, `find -mtime`, `command -v tool`, `sleep 30`, `git rebase|push|worktree`) run silently. A denied compound command names the denied segment and says the rest did not run. `.chemxrc` `guard.allow` lists command prefixes to leave alone; `guard.plainGit: true` drops the git status/add/commit nudges. Native Read, Edit, Write and Grep on project text files are warned by default (`nativeFileTools` in `.chemxrc`, or `CHEMX_NATIVE_FILE_TOOLS`): `warn` names the chemx equivalent, `block` denies the call, `allow` is silent.
575
592
 
576
593
  ### B. Bypass and Friction
577
594
  * When chemx truly cannot do the job, append `# chemx-bypass: <reason>` to the Bash command. The bypass and every guard denial are logged to `.chemx/friction.jsonl`.
@@ -585,17 +602,19 @@ Shared-workspace protocol. Several agents edit this checkout at once, and chemx
585
602
 
586
603
  1. Identity: Pass `--as=@<your session name>` on every `chemx team` command, or set `CHEMX_AGENT_ID`. Never act as the default `@agent`.
587
604
  2. Before choosing work: Run `chemx status`, `chemx team status`, `chemx team inbox @<you>` and `chemx team task list --status=in_progress` so you know what is claimed and locked.
588
- 3. Claim: Find or create the task (`chemx team task add "<title>" --needs=light|standard|deep`), then run `chemx team task claim <id> --as=@<you>`.
589
- 4. Lock before edit: Run `chemx team lock acquire <file> --as=@<you> --purpose="#<id>"` before the first edit of each file. If another handle holds the lock, do not edit that file: send `chemx team dm @<handle> "<msg>" --as=@<you>` or pick other work.
590
- 5. Edit through chemx: Read with `chemx read <path> --outline` or `--symbol=<name>`, search with `chemx q` or `chemx q -g "<text>"`, find files with `chemx f`, and edit with `chemx patch` or `chemx write`. Avoid cat, sed, grep and direct file writes where chemx has an equivalent.
591
- 6. Test: Run targeted specs with `chemx test <spec files> [-t name]`. Run a full suite only after parallel editors have finished.
592
- 7. Commit: Run `chemx commit <files> -m "<type>(<area>): <summary> (#<id>)"`. It stages and commits only the listed files, runs the repository pre-commit hook and refuses a file under another handle's lease. Do not use `git add -A`, `git commit -a`, `git stash`, git worktrees or side branches.
593
- 8. Wait: Use `chemx wait --task=<id>`, `chemx wait --lock-free=<file>` or `chemx wait --verify-idle` instead of sleep loops. The result is true as of the last poll only.
594
- 9. Progress: Record progress with `chemx team task comment <id> "<msg>" --as=@<you>` and decisions with `chemx team post "<msg>" --type=decision --task=<id> --as=@<you>`.
595
- 10. Inbox: Run `chemx team inbox @<you>` at every task boundary: after claiming, after committing, and before starting the next task.
596
- 11. Handoff: To pass a task to another agent, run `chemx team task handoff <id> @<to> --as=@<you>`. Only the assignee or creator can hand off.
597
- 12. Finish: Run the package gate (`chemx verify`), commit, release every lock with `chemx team lock release <file> --as=@<you>`, then run `chemx team task done <id> --target=<file> --as=@<you>`. If the gate refuses, mark the task blocked with a reason. Do not use `--force`.
598
- 13. Friction: When a chemx command misbehaves, you bypass it, or a flag is missing, file `chemx team task add "Friction: <what happened>" --needs=light --desc="<exact command and output>"` and keep going.
605
+ 3. Claim: Find or create the task (`chemx team task add "<title>" --needs=light|standard|deep`; `chemx task` is short for `chemx team task`), then run `chemx team task claim <id> --as=@<you>`. Read a task cheaply with `chemx team task show <id> --brief`. An epic with children goes in one file: `chemx task add --from tasks.yaml --dry-run` checks it, the same command without `--dry-run` files it. A long brief is stored with `chemx team brief <id> --file <md>`.
606
+ 4. Lock before edit: Run `chemx team lock acquire <file> --as=@<you> --purpose="#<id>"` before the first edit of each file; several files go in one call (`chemx team lock acquire a.js b.js --as=@<you>`, one result line per path). If another handle holds the lock, do not edit that file: send `chemx team dm @<handle> "<msg>" --as=@<you>` or pick other work.
607
+ 5. Edit through chemx: Read with `chemx read <path> --outline`, `--symbol=<name>` or a line range (`chemx read <path>:N-M --bare`), search with `chemx q` (symbols) or `chemx q -g "<text>" -C 3 --bare` (text), find files with `chemx f`, and edit with `chemx patch` or `chemx write`. Avoid cat, sed, grep and direct file writes where chemx has an equivalent.
608
+ 6. Workspace: Keep one-off files and branches out of the shared tree. A script or scratch file: `chemx scratch <name>` prints a path in your own git-ignored directory (`.chemx/scratch/<handle>/`), so another agent's file of the same name never replaces yours. Work that needs its own branch: `chemx worktree add <path> -b <branch> --link-modules`, then `chemx worktree remove <path>` once the branch has merged (it refuses an unmerged one). Run `chemx team` commands from the main checkout or with `--board=<dir>`, because a worktree can resolve another board. A mutation check: `chemx mutate <file> --apply "s/old/new/" --then "<command>"` runs the command on a changed file and puts the original back byte for byte. A long non-chemx command: `chemx run -- <command>` renews your file leases while it runs.
609
+ 7. Test: Run targeted specs with `chemx test <spec files> [-t name]`. Run a full suite only after parallel editors have finished.
610
+ 8. Commit: Run `chemx commit <files> -m "<type>(<area>): <summary> (#<id>)"`. It stages and commits only the listed files, runs the repository pre-commit hook and refuses a file under another handle's lease. From a worktree add `--board=<dir>` (the main checkout) so the task id is checked and the commit recorded on the board you claimed on; add `--no-skip-ci` when the commit must run CI (a commit to the default branch otherwise gets `[skip ci]` where the project sets commit.skipCi). Do not use `git add -A`, `git commit -a` or `git stash`; make a branch or worktree only through the Workspace step.
611
+ 9. Pull request: Once a pull request is open, read its checks with `chemx ci <pr> --wait` instead of `gh pr checks --watch` or a sleep loop: exit 0 means every check passed, 1 a check failed, 3 still pending or none reported yet. `chemx ci --protection` warns when the default branch requires no status checks, because `gh pr merge --auto` then merges at once. If the project keeps `docs/changelog.d/`, write `docs/changelog.d/<ticket>.md` (a `###` heading line, then bullets citing `#<id>`) and leave `docs/CHANGELOG.md` alone: `chemx changelog --check` validates the fragments and `chemx changelog` folds them into Unreleased.
612
+ 10. Wait: Use `chemx wait --task=<id>`, `chemx wait --lock-free=<file>`, `chemx wait --verify-idle`, `chemx wait --job=<pid>` (a backgrounded `chemx test ... & chemx wait --job=$!`; it exits with the job's own code) or `chemx wait --post-matching=<text> [--from=@<handle>]` (until a feed post matches) instead of sleep loops (a plain `sleep` of up to 120 seconds is fine). `--timeout=<30s|5m|2h>` bounds any of them. The result is true as of the last poll only.
613
+ 11. Progress: Record progress with `chemx team task comment <id> "<msg>" --as=@<you>` and decisions with `chemx team post "<msg>" --type=decision --task=<id> --as=@<you>`.
614
+ 12. Inbox: Run `chemx team inbox @<you>` at every task boundary: after claiming, after committing, and before starting the next task.
615
+ 13. Handoff: To pass a task to another agent, run `chemx team task handoff <id> @<to> --as=@<you>`. Only the assignee or creator can hand off.
616
+ 14. Finish: Run the package gate (`chemx verify`), commit, release every lock with `chemx team lock release <file> --as=@<you>`, then run `chemx team task done <id> --target=<file> --as=@<you>`. If the gate refuses, mark the task blocked with a reason. Do not use `--force`.
617
+ 15. Friction: When a chemx command misbehaves, you bypass it, or a flag is missing, file `chemx team task add "Friction: <what happened>" --needs=light --desc="<exact command and output>"` and keep going.
599
618
 
600
619
  Regenerate with `chemx pillars --protocol --write`; edits inside this block are overwritten.
601
620
  <!-- chemx:protocol end -->
package/README.md CHANGED
@@ -28,7 +28,7 @@
28
28
  npm install -g chemx # or: npx chemx <command>
29
29
 
30
30
  # 2. Check the install in your project
31
- chemx doctor # read-only; reports stale index rows, hooks and coverage
31
+ chemx doctor # read-only; kit commit vs CI pin, stale MCP servers by session, index, hooks, coverage
32
32
 
33
33
  # 3. Wire Claude Code to chemx (writes .claude/settings.json; --dry-run shows the change first)
34
34
  chemx install-hooks --host=claude --dry-run
@@ -204,8 +204,11 @@ chemx read src/components/m-card.vue --start=10 --end=40
204
204
 
205
205
  # Surgical file patching without full-file rewrites (literal replace, written through a temp file and rename, backup kept).
206
206
  # Multi-line edits: SEARCH/REPLACE blocks in a quoted heredoc ($, quotes and backticks stay literal).
207
- # Repeat the block for several edits; they apply in order and all-or-nothing. To edit text that
208
- # itself contains these markers, use 8 or more characters on all three marker lines.
207
+ # Repeat the block for several edits; they apply in order and all-or-nothing: a block that does not
208
+ # match, or that the guards refuse (parse, a removed declaration, a missing import), fails the call,
209
+ # is named ("Block 2 of 3") and writes nothing. Keep every edit to a file in one call. To edit text that
210
+ # itself contains these markers, use 8 or more characters on all three marker lines (a SEARCH holding a
211
+ # conflict left by a botched rebase works with 7: the file decides which divider ends the SEARCH, longest match).
209
212
  chemx patch <file> [--dry-run] <<'PATCH'
210
213
  <<<<<<< SEARCH
211
214
  const total = items.length;
@@ -219,8 +222,8 @@ chemx patch <file> --target="oldCode" --replacement="newCode"
219
222
  # Removing a top-level declaration (renames included) is refused unless named
220
223
  chemx patch <file> --target="oldName" --replacement="newName" --allow-remove=oldName
221
224
 
222
- # write creates files; replacing an existing one needs --overwrite. Unknown flags refuse.
223
- chemx write <file> - [--overwrite] [--dry-run] <<'EOF'
225
+ # write creates files (replacing one needs --overwrite; unknown flags refuse). Piped stdin is the content; `-` is optional.
226
+ chemx write <file> [--overwrite] [--dry-run] <<'EOF'
224
227
  export const a = 1;
225
228
  EOF
226
229
  chemx write <file> --content="export const a = 1;" [--overwrite] [--dry-run]
@@ -230,6 +233,9 @@ chemx write <file> --content=$'\nexport const b = 2;\n' --append
230
233
 
231
234
  # Literal text search; add paths to scope it. A missing path is an error, not an empty result.
232
235
  chemx q -g "useCardController" src/components docs
236
+ # `a|b` is literal text: --regex searches a regular expression (RE2 subset). -l prints each matching file
237
+ # once; -o --unique prints the distinct matched text. `chemx q <text>` with no symbol hit runs this search itself.
238
+ chemx q -g "--gs-[a-z0-9-]+" --regex -o --unique
233
239
 
234
240
  # Check that every chemx command named in your markdown exists (names only: flags and arguments are
235
241
  # not checked, and nothing is run). Exits 1 with file:line for each failure.
@@ -266,9 +272,10 @@ chemx use-task-filter
266
272
  ### 4. Database-Driven Multi-Agent Swarm Coordination
267
273
  Coordinate multi-agent swarms using the local SQLite store (`.chemx/index.db`) without multi-thousand-token markdown specification bloat:
268
274
  ```bash
269
- # Auto-triage: a full-scope `chemx audit` converts hazards into assignable team tasks by default.
270
- # Partial runs (--git, --fast, --staged, --no-index) never triage; `--no-triage` opts out.
271
- chemx audit
275
+ # Triage is opt-in: `chemx audit --triage` (or `autoTriage: true` in .chemxrc) converts hazards into
276
+ # assignable team tasks, at most 50 per run. A plain `chemx audit` files nothing.
277
+ # Partial runs (--git, --fast, --staged, --no-index) never triage.
278
+ chemx audit --triage
272
279
  chemx team task triage # triage on demand
273
280
 
274
281
  # List tasks assigned to a specific agent
@@ -312,16 +319,17 @@ All reference pages: [docs/INDEX.md](docs/INDEX.md).
312
319
 
313
320
  ## Language-Agnostic Polyglot Architecture
314
321
 
315
- Chemical X audits more than one language. The table below lists the extensions it parses and the checks each gets; any other file type is not audited. Depth differs by language: TypeScript, JavaScript, Vue and Svelte use a real parser, and C#, Python, Go and Rust use structural patterns, so they get fewer checks.
322
+ Chemical X audits more than one language. The table below lists the extensions it parses and the checks each gets; any other file type is not audited. Depth differs by language: TypeScript, JavaScript, Vue and Svelte use the Babel parser, PHP uses `php-parser`, C# uses structural patterns, and Python, Go and Rust get the universal text passes only, so they get fewer checks.
316
323
 
317
324
  ### Supported Language Ecosystems
318
325
 
319
326
  | Language / Framework | Extensions | Parser Engine | Capabilities |
320
327
  | :--- | :--- | :--- | :--- |
321
328
  | **C# / .NET 10** | `.cs` | Structural Regex / AST | Line budgets, MediatR handlers, shallow catch detection, fake in-memory stubs (`UseInMemoryDatabase`), `using` namespace indexing |
322
- | **Python** | `.py` | Structural Regex / AST | Line budgets, AI slop text patterns, fake assertions (`assert True`), `def`/`class` and `import` indexing |
323
- | **Go** | `.go` | Structural Regex / AST | Line budgets, mock data scanning, tautological assertions, `func`/`type` package indexing |
324
- | **Rust** | `.rs` | Structural Regex / AST | Line budgets, AI slop text patterns, secret scanning, struct and fn indexing |
329
+ | **Python** | `.py` | Universal text passes | Line budgets, AI slop text patterns, mock data and secret scanning, fake assertions (`assert True`), `def`/`class` and `import` indexing |
330
+ | **Go** | `.go` | Universal text passes | Line budgets, mock data scanning, tautological assertions, `func`/`type` package indexing |
331
+ | **Rust** | `.rs` | Universal text passes | Line budgets, AI slop text patterns, secret scanning, struct and fn indexing |
332
+ | **PHP / WordPress** | `.php` | `php-parser` AST (loaded only when a PHP file is audited) | Complexity, structural weight, nested ternary, swallowed catches, dynamic code and command execution, and in a WordPress plugin, theme or core tree: REST route permissions, missing nonces, and opt-in SQL, input, output, SSRF and redirect rules. See [docs/php-wordpress.md](docs/php-wordpress.md) |
325
333
  | **TypeScript / JavaScript** | `.ts`, `.tsx`, `.js`, `.jsx` | `@babel/parser` AST (`.vue` via `@vue/compiler-sfc`: every script block plus the template) | AST visitors across 11 audit categories, mapped to the 7 pillars; hook contracts, 2-stage booleans, lazy rule trees |
326
334
  | **Vue & Svelte** | `.vue`, `.svelte` | Babel + Template Registry | SFC template clone detection, reactive state verification |
327
335
 
@@ -334,6 +342,7 @@ Chemical X audits more than one language. The table below lists the extensions i
334
342
  * **Polyglot Fake Green Tests:** Catches tautological assertions in C# (`Assert.True(true)`), Python (`assert True`), and Go (`assert.True(t, true)`).
335
343
  2. **Tier 2: Deep Language-Specific Analyzers**
336
344
  * **Babel Engine:** Deep AST inspection for JS/TS/Vue/Svelte (runs on JS, TS, Vue and Svelte files only).
345
+ * **PHP Engine:** `php-parser` AST passes for `.php` files, plus WordPress passes (REST permissions, nonces, SQL, input flow) when a plugin, theme or core tree is detected. Some rules are default-off; `docs/php-wordpress.md` lists the shipped defaults, the per-plugin index and its caps.
337
346
  * **Control Flow & Guard Disciplines (`CONTROL_FLOW_CASCADE_GUARDS`, `CONTROL_FLOW_SILENT_GUARD`):** Flags silent bare returns in side-effecting code, and detects cascading early-return clusters (>= 3 guards), recommending extraction into `ruleTree` with 1-line callback aborts (Directive 3.H).
338
347
  * **Tailwind content coverage (`TAILWIND_UNSCANNED_CLASS_STRING`):** In a Tailwind v3 project, flags a string literal of 3 or more utility classes (2 or more of them Tailwind-shaped) in a `.js`, `.ts`, `.mjs` or `.cjs` file that no `content` glob of `tailwind.config` covers, since Tailwind purges classes it never scans. The config is read as text and never imported; Tailwind v4, a missing config or a non-literal `content` list keep the rule silent. HIGH, Directive 5.B. `options.tailwindContent` overrides the globs for tests.
339
348
  * **C# / Clean Architecture Analyzer:** Flags empty `catch (Exception) {}` blocks, simulated delays (`Task.Delay`), and monolithic controllers.
@@ -21,9 +21,12 @@ import { findForeignLease } from './edit-locks.js';
21
21
  import { syncLeasesAfterEdit } from './team/lease-renew.js';
22
22
  import { countLines } from './line-count.js';
23
23
 
24
+ // A refusal caused by one block of a multi-block patch carries { block, blocks } (cli/patch-block-blame.js).
25
+ const issueLine = (issue) => `${issue.block ? `Block ${issue.block} of ${issue.blocks}: ` : ''}${issue.file}: ${issue.reason}`;
26
+
24
27
  export class EditRefusedError extends Error {
25
28
  constructor(issues) {
26
- super(issues.map((i) => `${i.file}: ${i.reason}`).join('\n'));
29
+ super(issues.map(issueLine).join('\n'));
27
30
  this.name = 'EditRefusedError';
28
31
  this.issues = issues;
29
32
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The zsh unsplit-argument hint (#6773). zsh does not word-split an unquoted $var, so `chemx commit $files`
3
+ * receives ONE argument, "a.js b.js", and every command answered with a message that blamed the files
4
+ * (not tracked, File not found, matched nothing). One shared check lets commit, check and test say the
5
+ * real cause: an argument that has whitespace between words and is not a path on disk.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ export const UNSPLIT_HINT = 'one argument contains spaces: did a zsh variable not split? use an array or ${=var}';
11
+
12
+ const hasInnerSpace = (arg) => /\S\s+\S/.test(String(arg));
13
+
14
+ const diskLookup = (cwd) => (arg) => fs.existsSync(path.resolve(cwd, arg));
15
+
16
+ /**
17
+ * @param {string[]} args Paths or specs a command expected as separate arguments.
18
+ * @param {{ cwd?: string, exists?: (arg: string) => boolean }} [options] `exists` decides what is on disk (default: relative to `cwd`).
19
+ * @returns {string|null} The first argument that has spaces between words and is not a path on disk.
20
+ */
21
+ export const findUnsplitArgument = (args, { cwd = process.cwd(), exists = diskLookup(cwd) } = {}) => {
22
+ const candidates = (args ?? []).filter(hasInnerSpace);
23
+ return candidates.find((arg) => !exists(arg)) ?? null;
24
+ };
25
+
26
+ /** @returns {string} UNSPLIT_HINT when an argument looks unsplit, else ''. */
27
+ export const unsplitHint = (args, options = {}) => (findUnsplitArgument(args, options) === null ? '' : UNSPLIT_HINT);
28
+
29
+ /** @returns {string} `message` with the hint in parentheses when an argument looks unsplit. */
30
+ export const withUnsplitHint = (message, args, options = {}) => {
31
+ const hint = unsplitHint(args, options);
32
+ return hint ? `${message} (${hint})` : message;
33
+ };
@@ -10,6 +10,14 @@ const formatGate = (gate) => {
10
10
  return `gate: ${verdict}${basis}${note}`;
11
11
  };
12
12
 
13
+ // One line per language, only when more than one language was scanned.
14
+ const formatLanguageLines = (coverage) => {
15
+ const entries = Object.entries(coverage?.byLanguage ?? {});
16
+ const isMultiLanguage = entries.length > 1;
17
+ if (!isMultiLanguage) return [];
18
+ return entries.map(([language, c]) => ` ${language}: ${c.files} files, ${c.ast} ast, ${c.textOnly} text-only, ${c.parseErrors} parse errors`);
19
+ };
20
+
13
21
  export const formatPlainAuditSummary = (summary) => {
14
22
  const { critical, highMedium, low } = summary.hazards;
15
23
  const total = critical + highMedium + low;
@@ -21,6 +29,7 @@ export const formatPlainAuditSummary = (summary) => {
21
29
  for (const { rule, baseline, current } of summary.gate.regressions ?? []) {
22
30
  lines.push(` regression: ${rule} ${baseline} -> ${current}`);
23
31
  }
32
+ lines.push(...formatLanguageLines(summary.coverage));
24
33
  lines.push('detail: chemx audit --json | chemx audit --unroll | chemx q --hazards');
25
34
  return `${lines.join('\n')}\n`;
26
35
  };
@@ -14,8 +14,8 @@ const countHazards = (violations = []) => {
14
14
 
15
15
  const summarizeCoverageForCard = (coverage) => {
16
16
  if (!coverage) return null;
17
- const { files, astPct, parseErrors, textOnly, isPartial, sfc } = coverage;
18
- return { files, astPct, parseErrors, textOnly, isPartial, sfcTemplates: sfc?.templatesParsed ?? 0 };
17
+ const { files, astPct, parseErrors, textOnly, isPartial, sfc, byLanguage = {} } = coverage;
18
+ return { files, astPct, parseErrors, textOnly, isPartial, sfcTemplates: sfc?.templatesParsed ?? 0, byLanguage };
19
19
  };
20
20
 
21
21
  export const buildAuditSummary = (report, { projectRoot, scope }) => {
@@ -13,10 +13,33 @@ export const COVERAGE_KINDS = Object.freeze({
13
13
 
14
14
  export const createCoverageCollector = () => ({ files: [] });
15
15
 
16
- /** Records one file. `sfc` is { scriptBlocks, hasTemplate, isTemplateParsed } for .vue files. */
17
- export const recordFileCoverage = (collector, { relativePath, kind, sfc = null }) => {
16
+ /**
17
+ * Records one file. `sfc` is { scriptBlocks, hasTemplate, isTemplateParsed } for .vue files;
18
+ * `language` (language id) and `reason` (why the file was not fully parsed) are optional.
19
+ */
20
+ export const recordFileCoverage = (collector, { relativePath, kind, sfc = null, language = null, reason = null }) => {
18
21
  if (!collector) return;
19
- collector.files.push({ relativePath, kind, sfc });
22
+ collector.files.push({ relativePath, kind, sfc, language, reason });
23
+ };
24
+
25
+ const BUCKET_FIELD_BY_KIND = {
26
+ [COVERAGE_KINDS.AST]: 'ast',
27
+ [COVERAGE_KINDS.TEXT_ONLY]: 'textOnly',
28
+ [COVERAGE_KINDS.PARSE_ERROR]: 'parseErrors'
29
+ };
30
+
31
+ const summarizeByLanguage = (entries) => {
32
+ const byLanguage = {};
33
+ for (const entry of entries) {
34
+ if (!entry.language) continue;
35
+ const bucket = byLanguage[entry.language] ?? { files: 0, ast: 0, textOnly: 0, parseErrors: 0 };
36
+ bucket.files += 1;
37
+ const field = BUCKET_FIELD_BY_KIND[entry.kind];
38
+ const isCounted = Boolean(field);
39
+ if (isCounted) bucket[field] += 1;
40
+ byLanguage[entry.language] = bucket;
41
+ }
42
+ return byLanguage;
20
43
  };
21
44
 
22
45
  const isPartialEntry = (entry) => {
@@ -42,6 +65,7 @@ export const summarizeCoverage = (collector) => {
42
65
  scriptBlocks: sfcEntries.reduce((sum, e) => sum + (e.sfc.scriptBlocks ?? 0), 0),
43
66
  templatesParsed: sfcEntries.filter((e) => e.sfc.isTemplateParsed).length
44
67
  },
68
+ byLanguage: summarizeByLanguage(entries),
45
69
  isPartial: partialFiles.length > 0,
46
70
  partialFiles: partialFiles.slice(0, 20)
47
71
  };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Inconclusive audit results: a file whose language has an AST parser but whose coverage record
3
+ * is not "ast" (--fast, parser unavailable, minified, parse error) was only partly examined, so a
4
+ * low hazard count says nothing about it. Shared by `chemx check`, the edit guardrails and
5
+ * `task done`. An empty file is conclusive; a language with no AST parser is never inconclusive.
6
+ */
7
+ import path from 'node:path';
8
+ import { LANGUAGE_DEFINITIONS, getLanguageByExtension, isSourceFile } from '../languages.js';
9
+
10
+ const AST_PARSERS = new Set(['babel', 'php']);
11
+ const NO_REASON = 'no reason recorded';
12
+
13
+ const hasAstParser = (language) => language === 'php' || AST_PARSERS.has(LANGUAGE_DEFINITIONS[language]?.parser);
14
+
15
+ /** @param {{ kind?: string, language?: string|null, reason?: string|null }|null|undefined} coverage */
16
+ export const isInconclusive = (coverage) => {
17
+ const hasLanguage = Boolean(coverage && coverage.language);
18
+ if (!hasLanguage) return false;
19
+ const isCovered = coverage.kind === 'ast' || coverage.kind === 'empty';
20
+ return hasAstParser(coverage.language) && !isCovered;
21
+ };
22
+
23
+ /** The "inconclusive: <reason>" text for a coverage record, or null when the result is conclusive. */
24
+ export const inconclusiveReason = (coverage) => (isInconclusive(coverage) ? (coverage.reason || coverage.kind || NO_REASON) : null);
25
+
26
+ /**
27
+ * True for a file the audit accepts: a known source extension, or one whose language was added to
28
+ * LANGUAGE_DEFINITIONS after load (the extension table is built once, so this also lets specs
29
+ * register a language). Tests count as source, matching auditFile.
30
+ */
31
+ export const isAuditableSource = (name) => {
32
+ const isKnownSource = isSourceFile(name, { includeTests: true });
33
+ const isExcluded = name.includes('.min.') || name.endsWith('.d.ts');
34
+ const isRegistered = !isExcluded && Boolean(getLanguageByExtension(path.extname(name)));
35
+ return isKnownSource || isRegistered;
36
+ };
@@ -63,6 +63,9 @@ const DEFAULT_TIER_PATTERNS = Object.freeze({
63
63
  view: [/(?:^|[\\/])views[\\/]/, /(?:^|[\\/])pages[\\/]/, /View\.[tj]sx?$/]
64
64
  });
65
65
  const VIEW_MARKUP_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx', '.svelte']);
66
+ // PHP is view markup only as a WordPress partial or template; PHP has no molecule tier.
67
+ const PHP_VIEW_DIRECTORY = /(?:^|[\\/])(?:partials|templates)[\\/]/;
68
+ const isPhpPath = (relativePath) => path.extname(relativePath).toLowerCase() === '.php';
66
69
 
67
70
  /** Lines as `wc -l` counts them: a trailing newline does not open a new line. */
68
71
  export const countLines = (content) => {
@@ -141,6 +144,7 @@ const matchesConfiguredTier = (relPath, globs = []) => globs.some((glob) => glob
141
144
  /** 'molecule' | 'view' | null. `.chemxrc` `tiers: { molecule: [globs], view: [globs] }` extends the defaults. */
142
145
  export const resolveFileTier = (relativePath, config = {}) => {
143
146
  const normalized = relativePath.split(path.sep).join('/');
147
+ if (isPhpPath(normalized)) return PHP_VIEW_DIRECTORY.test(normalized) ? 'view' : null;
144
148
  const baseName = path.basename(normalized);
145
149
  const tiers = config.tiers || {};
146
150
  const isMolecule = DEFAULT_TIER_PATTERNS.molecule.some((re) => re.test(normalized)) ||
@@ -151,4 +155,8 @@ export const resolveFileTier = (relativePath, config = {}) => {
151
155
  };
152
156
 
153
157
  /** View budgets apply to markup-bearing view entries, never to .ts/.js controllers. */
154
- export const isViewMarkupFile = (relativePath) => VIEW_MARKUP_EXTENSIONS.has(path.extname(relativePath));
158
+ export const isViewMarkupFile = (relativePath) => {
159
+ const normalized = relativePath.split(path.sep).join('/');
160
+ if (isPhpPath(normalized)) return PHP_VIEW_DIRECTORY.test(normalized);
161
+ return VIEW_MARKUP_EXTENSIONS.has(path.extname(relativePath));
162
+ };
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { RULESET_VERSION, resolveRuleRevision } from './rule-revisions.js';
4
4
  import { RULE_REGISTRY } from './rules-registry.js';
5
+ import { LANGUAGE_DEFINITIONS } from '../languages.js';
5
6
 
6
7
  /**
7
8
  * chemx-ratchet.json, format version 2:
@@ -12,10 +13,38 @@ import { RULE_REGISTRY } from './rules-registry.js';
12
13
  * `allRules: true` means `revisions` lists every rule that existed at recording time,
13
14
  * so a registered rule missing from it is new and is adopted even if nobody bumped its revision.
14
15
  * Every full scan upgrades the scope to the current rule set (see upgradeScope).
16
+ * `languages` lists the file extensions a scope was recorded over. A scope without it is
17
+ * read as the extensions scanned before PHP support (LEGACY_LANGUAGES). Counts from
18
+ * violations in a file whose extension is not in that set are adopted, not gated, and the
19
+ * extension is then recorded; violations without a file path are always gated.
15
20
  */
16
21
  export const RATCHET_FILE = 'chemx-ratchet.json';
17
22
  const RATCHET_VERSION = 2;
18
23
 
24
+ const PRE_PHP_EXCLUDED = new Set(['.php']);
25
+ const allExtensions = () => Object.values(LANGUAGE_DEFINITIONS).flatMap((lang) => [...lang.extensions]);
26
+ const LEGACY_LANGUAGES = allExtensions().filter((ext) => !PRE_PHP_EXCLUDED.has(ext)).sort();
27
+
28
+ /** The extensions a full scan covers now (what a scope records). */
29
+ const currentLanguages = () => allExtensions().sort();
30
+
31
+ const recordedLanguages = (entry) => (Array.isArray(entry.languages) ? entry.languages : LEGACY_LANGUAGES);
32
+
33
+ const mergeLanguages = (entry, adopted = []) => {
34
+ const adoptedExtensions = adopted.flatMap((item) => item.extensions ?? []);
35
+ return [...new Set([...recordedLanguages(entry), ...currentLanguages(), ...adoptedExtensions])].sort();
36
+ };
37
+
38
+ const extensionOf = (violation) => {
39
+ const file = violation.filePath ?? violation.file;
40
+ return typeof file === 'string' ? path.extname(file).toLowerCase() : '';
41
+ };
42
+
43
+ const isOutsideLanguages = (violation, languages) => {
44
+ const ext = extensionOf(violation);
45
+ return ext !== '' && !languages.has(ext);
46
+ };
47
+
19
48
  export const countViolationsByRule = (violations = []) => {
20
49
  const counts = {};
21
50
  for (const v of violations) counts[v.rule] = (counts[v.rule] ?? 0) + 1;
@@ -69,7 +98,7 @@ const readExistingOrEmpty = (projectRoot) => {
69
98
  export const writeRatchet = (projectRoot, { scope, violations }) => {
70
99
  const ratchet = readExistingOrEmpty(projectRoot);
71
100
  const rules = sortKeys(countViolationsByRule(violations));
72
- ratchet.scopes[scope] = { ruleset: RULESET_VERSION, allRules: true, rules, revisions: currentRevisions(rules) };
101
+ ratchet.scopes[scope] = { ruleset: RULESET_VERSION, allRules: true, rules, revisions: currentRevisions(rules), languages: currentLanguages() };
73
102
  persistRatchet(projectRoot, ratchet);
74
103
  return ratchet.scopes[scope];
75
104
  };
@@ -86,7 +115,8 @@ const upgradeScope = (entry, adopted) => {
86
115
  ruleset: Math.max(entry.ruleset ?? 1, RULESET_VERSION),
87
116
  allRules: true,
88
117
  rules: sortKeys(rules),
89
- revisions: currentRevisions(rules)
118
+ revisions: currentRevisions(rules),
119
+ languages: mergeLanguages(entry, adopted)
90
120
  };
91
121
  };
92
122
 
@@ -94,7 +124,8 @@ const upgradeScope = (entry, adopted) => {
94
124
  export const needsScopeUpgrade = (entry, adopted = []) => {
95
125
  if (!entry) return false;
96
126
  const upgraded = upgradeScope(entry, adopted);
97
- const isSame = JSON.stringify(upgraded) === JSON.stringify({ ruleset: entry.ruleset, allRules: entry.allRules, rules: entry.rules, revisions: entry.revisions });
127
+ const stored = { ruleset: entry.ruleset, allRules: entry.allRules, rules: entry.rules, revisions: entry.revisions, languages: entry.languages };
128
+ const isSame = JSON.stringify(upgraded) === JSON.stringify(stored);
98
129
  return !isSame;
99
130
  };
100
131
 
@@ -116,7 +147,10 @@ const recordedRevision = (entry, rule) => {
116
147
  };
117
148
 
118
149
  const compareToBaseline = (entry, violations) => {
150
+ const languages = new Set(recordedLanguages(entry));
119
151
  const current = countViolationsByRule(violations);
152
+ const outsideViolations = violations.filter((v) => isOutsideLanguages(v, languages));
153
+ const outside = countViolationsByRule(outsideViolations);
120
154
  const allRules = new Set([...Object.keys(current), ...Object.keys(entry.rules)]);
121
155
  const regressions = [];
122
156
  const adopted = [];
@@ -125,9 +159,14 @@ const compareToBaseline = (entry, violations) => {
125
159
  const count = current[rule] ?? 0;
126
160
  const isNewerRevision = revision > recordedRevision(entry, rule);
127
161
  const baseline = entry.rules[rule] ?? 0;
128
- if (isNewerRevision) adopted.push({ rule, baseline, current: count, revision });
129
- const isRegression = !isNewerRevision && count > baseline;
130
- if (isRegression) regressions.push({ rule, baseline, current: count });
162
+ const outsideCount = outside[rule] ?? 0;
163
+ const gatedCount = count - outsideCount;
164
+ const extensions = [...new Set(outsideViolations.filter((v) => v.rule === rule).map(extensionOf))];
165
+ if (isNewerRevision) adopted.push({ rule, baseline, current: count, revision, extensions });
166
+ const isLanguageAdoption = !isNewerRevision && outsideCount > 0;
167
+ if (isLanguageAdoption) adopted.push({ rule, baseline, current: baseline + outsideCount, revision, reason: 'language', extensions });
168
+ const isRegression = !isNewerRevision && gatedCount > baseline;
169
+ if (isRegression) regressions.push({ rule, baseline, current: gatedCount });
131
170
  }
132
171
  return { regressions, adopted };
133
172
  };
@@ -3,17 +3,20 @@
3
3
  * (finding config-not-honored):
4
4
  * rules: { "RULE_ID": "off" | "low" | "medium" | "high" | "critical" }
5
5
  * overrides: [{ files: "glob" | ["globs"], rules: { "RULE_ID": "off" | <severity> } }]
6
+ * A setting of "on" (or true) enables a default-off rule and keeps the engine's severities.
6
7
  * ai-slop-detection: "off" drops AI_SLOP_* rules, "medium" caps them at MEDIUM.
7
8
  * pillars (from .chemx/config.json): a disabled product pillar drops its rules.
8
9
  * Later overrides win over earlier ones, and every override wins over `rules`.
9
10
  */
10
11
  import { globToRegExp } from './line-budgets.js';
11
12
  import { resolvePillarDisabledRules } from './rule-pillars.js';
13
+ import { RULE_REGISTRY } from './rules-registry.js';
12
14
 
13
15
  const SEVERITIES = new Map([['low', 'LOW'], ['medium', 'MEDIUM'], ['high', 'HIGH'], ['critical', 'CRITICAL']]);
14
16
  const SEVERITY_RANK = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
15
17
  const RULE_ID_SHAPE = /^[A-Z][A-Z0-9_]+$/;
16
18
  const OFF = 'off';
19
+ const ON = 'on';
17
20
 
18
21
  const isSlopRule = (rule) => typeof rule === 'string' && rule.startsWith('AI_SLOP_');
19
22
 
@@ -21,6 +24,8 @@ const normalizeSetting = (value) => {
21
24
  const lowered = String(value).toLowerCase();
22
25
  const isOff = lowered === OFF || value === false;
23
26
  if (isOff) return OFF;
27
+ const isOn = lowered === ON || value === true;
28
+ if (isOn) return ON;
24
29
  return SEVERITIES.get(lowered) ?? null;
25
30
  };
26
31
 
@@ -60,17 +65,26 @@ const applySlopPolicy = (violation, policy) => {
60
65
  return isCapped ? { ...violation, severity: 'MEDIUM' } : violation;
61
66
  };
62
67
 
63
- /** Applies `rules`, `overrides` and `ai-slop-detection` to one file's violations. */
64
- export const applyRuleOverrides = (violations, relativePath, config = {}) => {
68
+ /**
69
+ * Applies `rules`, `overrides` and `ai-slop-detection` to one file's violations.
70
+ * A rule is off by default when its registry entry has `isDefaultOff: true` or the caller's
71
+ * `defaultOff` set names it, until a setting names it: "on" (or true) enables it and keeps the
72
+ * engine's per-violation severities, a severity enables it and overwrites them.
73
+ * options: { registry (default RULE_REGISTRY), defaultOff (Set of rule ids) }.
74
+ */
75
+ export const applyRuleOverrides = (violations, relativePath, config = {}, options = {}) => {
76
+ const { registry = RULE_REGISTRY, defaultOff = new Set() } = options;
65
77
  const settings = resolveRuleSettings(relativePath, config);
66
78
  const pillarDisabled = resolvePillarDisabledRules(config.pillars);
67
79
  const slopPolicy = String(config.rules?.aiSlopDetection ?? config.rules?.['ai-slop-detection'] ?? 'critical').toLowerCase();
68
80
  const result = [];
69
81
  for (const violation of violations) {
70
82
  const setting = settings[violation.rule];
71
- const isOff = setting === OFF || (!setting && pillarDisabled.has(violation.rule));
83
+ const isDefaultOff = registry[violation.rule]?.isDefaultOff === true || defaultOff.has(violation.rule);
84
+ const isOff = setting === OFF || (!setting && (isDefaultOff || pillarDisabled.has(violation.rule)));
72
85
  if (isOff) continue;
73
- const withSeverity = setting ? { ...violation, severity: setting } : violation;
86
+ const isSeverityOverride = Boolean(setting) && setting !== ON;
87
+ const withSeverity = isSeverityOverride ? { ...violation, severity: setting } : violation;
74
88
  const afterSlop = setting ? withSeverity : applySlopPolicy(withSeverity, slopPolicy);
75
89
  if (afterSlop) result.push(afterSlop);
76
90
  }