arkgate 4.8.23 → 4.8.25

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 (201) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +41 -91
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +707 -121
  6. package/bin/ark-mcp-runtime.mjs +448 -169
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +42 -22
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/adoption-stance.mjs +2 -2
  13. package/bin/lib/adr-path.mjs +3 -3
  14. package/bin/lib/agent-gates.mjs +0 -61
  15. package/bin/lib/agent-homes.mjs +12 -21
  16. package/bin/lib/agent-projection-command.mjs +9 -5
  17. package/bin/lib/ambient-state.mjs +9 -30
  18. package/bin/lib/analysis-engine.mjs +15 -11
  19. package/bin/lib/architecture-scan.mjs +61 -15
  20. package/bin/lib/ark-order-doctor.mjs +4 -29
  21. package/bin/lib/ark-order-facts.mjs +319 -17
  22. package/bin/lib/ark-order-sensors.mjs +28 -101
  23. package/bin/lib/ark-order-types.mjs +3 -2
  24. package/bin/lib/ark-run-doctor.mjs +17 -28
  25. package/bin/lib/ark-run-facts.mjs +461 -48
  26. package/bin/lib/ark-run-sensors.mjs +6 -4
  27. package/bin/lib/arkgate-pins.mjs +91 -0
  28. package/bin/lib/arkrule-file-hints.mjs +2 -47
  29. package/bin/lib/arkrules-contract.mjs +124 -133
  30. package/bin/lib/arkrules-sensors.mjs +55 -252
  31. package/bin/lib/check-args.mjs +85 -1
  32. package/bin/lib/ci-and-commands.mjs +24 -78
  33. package/bin/lib/ci-merge-boundary.mjs +1 -1
  34. package/bin/lib/class-source-scan.mjs +479 -0
  35. package/bin/lib/clone-detection.mjs +474 -0
  36. package/bin/lib/codex-home.mjs +5 -3
  37. package/bin/lib/config-contract-slices.mjs +739 -0
  38. package/bin/lib/config-contract.mjs +42 -607
  39. package/bin/lib/config-extras.mjs +16 -22
  40. package/bin/lib/config-warnings.mjs +32 -0
  41. package/bin/lib/contract-smells.mjs +33 -15
  42. package/bin/lib/core-ratchet.mjs +0 -2
  43. package/bin/lib/dashboard-cli.mjs +62 -0
  44. package/bin/lib/deep-module-coach.mjs +2 -4
  45. package/bin/lib/deploy-path.mjs +13 -38
  46. package/bin/lib/design-delta.mjs +1 -1
  47. package/bin/lib/design-smells.mjs +2 -2
  48. package/bin/lib/diagnostic-catalog.mjs +23 -9
  49. package/bin/lib/doctor-advisories.mjs +78 -8
  50. package/bin/lib/doctor-green-cite.mjs +1 -1
  51. package/bin/lib/doctor-human.mjs +1 -1
  52. package/bin/lib/doctor-next-actions.mjs +19 -4
  53. package/bin/lib/doctor-plan.mjs +9 -13
  54. package/bin/lib/duplication-io.mjs +572 -0
  55. package/bin/lib/effective-contract-load.mjs +282 -83
  56. package/bin/lib/enforcement-profiles.mjs +40 -4
  57. package/bin/lib/entry-points-io.mjs +424 -0
  58. package/bin/lib/extra-merge-teeth.mjs +36 -0
  59. package/bin/lib/field-install.mjs +38 -14
  60. package/bin/lib/first-run-help.mjs +10 -5
  61. package/bin/lib/flat-parent-importers.mjs +139 -0
  62. package/bin/lib/flat-parent-pilot.mjs +40 -100
  63. package/bin/lib/gate-files.mjs +58 -90
  64. package/bin/lib/git-change-scope.mjs +109 -0
  65. package/bin/lib/github-enforcement.mjs +166 -29
  66. package/bin/lib/golden-pattern.mjs +1 -1
  67. package/bin/lib/hook-templates.mjs +17 -15
  68. package/bin/lib/host-support-matrix.mjs +3 -1
  69. package/bin/lib/html-report-advisories.mjs +4 -0
  70. package/bin/lib/html-report-depth.mjs +2 -2
  71. package/bin/lib/html-report.mjs +2 -2
  72. package/bin/lib/import-graph-projection.mjs +112 -0
  73. package/bin/lib/import-resolve.mjs +13 -1
  74. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  75. package/bin/lib/install-migrate.mjs +50 -73
  76. package/bin/lib/invariant-coverage-io.mjs +4 -3
  77. package/bin/lib/invariant-coverage.mjs +85 -11
  78. package/bin/lib/invariant-probe-cli.mjs +347 -0
  79. package/bin/lib/invariant-probe-io.mjs +165 -0
  80. package/bin/lib/invariant-probe-runner.mjs +268 -0
  81. package/bin/lib/invariant-probe-sites.mjs +174 -0
  82. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  83. package/bin/lib/invariant-probe.mjs +461 -0
  84. package/bin/lib/invariant-tests-path.mjs +16 -22
  85. package/bin/lib/literal-path-drift.mjs +4 -2
  86. package/bin/lib/managed-upgrade.mjs +2 -8
  87. package/bin/lib/mcp-adoption.mjs +13 -6
  88. package/bin/lib/mcp-hook-payload.mjs +403 -63
  89. package/bin/lib/orphan-modules-io.mjs +458 -0
  90. package/bin/lib/orphan-modules.mjs +251 -0
  91. package/bin/lib/outside-importers.mjs +197 -0
  92. package/bin/lib/package-manager.mjs +60 -2
  93. package/bin/lib/parse-health.mjs +1 -1
  94. package/bin/lib/passthrough-commands.mjs +83 -0
  95. package/bin/lib/persistence-write-hint.mjs +106 -0
  96. package/bin/lib/pin-dependent-gates.mjs +117 -0
  97. package/bin/lib/policy-delta-io.mjs +219 -14
  98. package/bin/lib/post-green-path.mjs +1 -1
  99. package/bin/lib/prepare-change.mjs +115 -7
  100. package/bin/lib/prepare-write.mjs +2 -2
  101. package/bin/lib/presets.mjs +59 -73
  102. package/bin/lib/product-copy.mjs +1 -6
  103. package/bin/lib/project-root.mjs +31 -62
  104. package/bin/lib/remediation.mjs +23 -4
  105. package/bin/lib/resolved-candidate-facts.mjs +405 -117
  106. package/bin/lib/rules-inventory.mjs +22 -3
  107. package/bin/lib/rules-under-contract.mjs +247 -9
  108. package/bin/lib/scan-files.mjs +36 -4
  109. package/bin/lib/schema-validation.mjs +130 -0
  110. package/bin/lib/sensor-promote-cli.mjs +2 -11
  111. package/bin/lib/sensor-promote-io.mjs +33 -22
  112. package/bin/lib/sensor-promotion.mjs +1 -1
  113. package/bin/lib/skill-catalog-apply.mjs +1 -1
  114. package/bin/lib/skill-install.mjs +42 -38
  115. package/bin/lib/snippet-analysis.mjs +140 -6
  116. package/bin/lib/source-policy.mjs +34 -0
  117. package/bin/lib/start-install-recovery.mjs +72 -2
  118. package/bin/lib/start-preview.mjs +31 -1
  119. package/bin/lib/states-transitions-presence.mjs +3 -3
  120. package/bin/lib/status-command.mjs +35 -22
  121. package/bin/lib/status-manifest.mjs +50 -30
  122. package/bin/lib/status-transition-catalog.mjs +0 -6
  123. package/bin/lib/suggestions.mjs +3 -3
  124. package/bin/lib/team-parliament-io.mjs +76 -88
  125. package/bin/lib/team-parliament.mjs +21 -10
  126. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  127. package/bin/lib/upgrade-command.mjs +2 -24
  128. package/bin/lib/upgrade-outcome.mjs +1 -1
  129. package/bin/lib/upstream-report.mjs +21 -10
  130. package/bin/lib/violations.mjs +10 -9
  131. package/bin/lib/weakest-link.mjs +13 -0
  132. package/bin/lib/write-path-capabilities.mjs +6 -4
  133. package/dist/chunk-A5EIY3HI.cjs +5 -0
  134. package/dist/chunk-AHACEKSV.js +16 -0
  135. package/dist/chunk-SVSQVGFJ.js +5 -0
  136. package/dist/chunk-UN5EH4QV.cjs +16 -0
  137. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
  138. package/dist/configTypes-GPvvdleM.d.ts +257 -0
  139. package/dist/eslint/index.cjs +18 -5
  140. package/dist/eslint/index.d.cts +368 -0
  141. package/dist/eslint/index.d.ts +139 -156
  142. package/dist/eslint/index.js +11 -5
  143. package/dist/index.cjs +36 -35
  144. package/dist/index.d.cts +5547 -0
  145. package/dist/index.d.ts +3283 -198
  146. package/dist/index.js +36 -35
  147. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  148. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  149. package/dist/nestjs/index.cjs +1 -18
  150. package/dist/nestjs/index.d.cts +22 -0
  151. package/dist/nestjs/index.d.ts +1 -3
  152. package/dist/nestjs/index.js +1 -18
  153. package/dist/order/index.cjs +1 -1
  154. package/dist/order/index.d.cts +155 -0
  155. package/dist/order/index.d.ts +15 -3
  156. package/dist/order/index.js +1 -1
  157. package/dist/runtime/index.cjs +13 -30
  158. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
  159. package/dist/runtime/index.d.ts +2741 -133
  160. package/dist/runtime/index.js +13 -30
  161. package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
  162. package/dist/types-CnauMQQL.d.ts +1800 -0
  163. package/docs/README.md +3 -1
  164. package/docs/agent-guide.md +217 -16
  165. package/docs/ai-gates.md +174 -26
  166. package/docs/arkorder.md +32 -15
  167. package/docs/brownfield-adoption.md +5 -1
  168. package/docs/configuration.md +230 -31
  169. package/docs/develop.md +2 -1
  170. package/docs/diagnostics.md +212 -19
  171. package/docs/package-surface.md +35 -9
  172. package/docs/product-voice.md +7 -1
  173. package/package.json +69 -19
  174. package/schemas/ark.analysis-result.schema.json +15 -0
  175. package/schemas/ark.config.schema.json +77 -12
  176. package/schemas/ark.invariant-probe.schema.json +368 -0
  177. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  178. package/schemas/ark.status-manifest.schema.json +22 -0
  179. package/server.json +3 -9
  180. package/templates/agent-skills/README.md +1 -1
  181. package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
  182. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  183. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  184. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  185. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  186. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  187. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  188. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  189. package/templates/arkrules/PersistenceAdapters.json +1 -1
  190. package/templates/arkrules/PresentationAdapters.json +1 -1
  191. package/templates/skills/ark-adopt.md +5 -1
  192. package/templates/skills/ark-contract.md +17 -0
  193. package/templates/skills/ark-coverage.md +2 -0
  194. package/templates/skills/ark-explore.md +7 -0
  195. package/templates/skills/ark-fix.md +20 -0
  196. package/templates/skills/ark-order.md +4 -2
  197. package/templates/skills/ark-place.md +8 -0
  198. package/templates/skills/ark-runtime.md +6 -1
  199. package/bin/lib/ark-order-error.mjs +0 -18
  200. package/bin/lib/ark-order-invariants.mjs +0 -346
  201. package/dist/types-Ik_OakcI.d.ts +0 -183
package/docs/README.md CHANGED
@@ -73,7 +73,9 @@ These are **not** the day-to-day product path. They stay in the repo for evidenc
73
73
  | Field adoption kit (scaffolding, not closed) | [field/](field/) |
74
74
  | Runtime hardening (experimental) | [production-hardening.md](production-hardening.md) |
75
75
 
76
- Current release: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.23`; confirm npm with `npm view arkgate@4.8.23 version`; does not close `K01`).
76
+ Current release: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.25`; confirm npm with `npm view arkgate@4.8.25 version`; does not close `K01`).
77
+ Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.24`).
78
+ Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.23`).
77
79
  Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.22`).
78
80
  Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.21`).
79
81
  Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.20`). Plain-English history: [arkgate.online/changelog](https://www.arkgate.online/changelog/).
@@ -41,7 +41,7 @@ the project API is unusable. See the distribution and completeness boundary in
41
41
  ark start → ark start --apply → ark-check --doctor
42
42
  day to day: compact router / MCP place + validate + check
43
43
  guided work: install skill pack → /ark-autopilot
44
- optional: arkRules map + arkrules/*.json (intra-layer; starts advisory)
44
+ optional: arkRules map (one path or a list) + arkrules/*.json, or childSlices.arkRulesFile at a slice root (intra-layer; starts advisory)
45
45
  ```
46
46
 
47
47
  `arkgate-check --doctor` shows what's wrong and what to do first. From **4.0.0**, doctor may
@@ -207,6 +207,16 @@ npx ark status --json
207
207
  npx ark status --json --expected-root /abs/project/root
208
208
  ```
209
209
 
210
+ `--config <file>` reads a non-default contract (same for `ark agents-md --config <file>`).
211
+ When the resolved config fails the shared contract validator, status adds
212
+ `contract: { valid: false, errors: [...] }` and the next action is `fix-config` — the same
213
+ config every enforcing surface refuses is never reported as ready. `ark-check --json` (and
214
+ `--doctor --json`) print `{ "ok": false, "error": "CONFIG_INVALID", "configPath", "messages" }`
215
+ on stdout for that case (exit 2), so a JSON consumer always gets the reason. Any other fatal
216
+ error under `--json` prints `{ "ok": false, "error": "POLICY_BASE_UNREADABLE" | "CHECK_ERROR", "message" }`
217
+ (also exit 2); `POLICY_BASE_UNREADABLE` means the policy base ref (for example `GITHUB_BASE_REF`)
218
+ could not be read in this checkout — fetch it or pass `--base <ref>`.
219
+
210
220
  MCP parity tool: **`ark_status`** (same envelope; pass `project.expectedRoot` after `ark_identity`).
211
221
  Schema: `arkgate/schema/status-manifest`. Never prompts; under `CI=1` JSON is forced. **Not a
212
222
  score** — counts, honesty modes, and residual ids only. Write-path interpretation of activation vs
@@ -312,14 +322,35 @@ npx ark-check --ratchet-cores # require populated core layers
312
322
  npx ark-check --watch # debounced re-check when governed files change
313
323
  ```
314
324
 
325
+ **Package pin:** `ark start` and `ark init` both pin `arkgate` in `package.json`
326
+ `devDependencies` (and install it unless `--skip-package-manager`) before writing the CI workflow
327
+ and host hooks, because those call the local `ark-check` / `arkgate-mcp` bins. `--no-install`
328
+ skips the pin; without a `package.json` setup prints a warning instead. When arkgate is not a
329
+ local dependency (no pin, no `node_modules/arkgate`), every generated command runs
330
+ `npx -y -p arkgate@<exact version> <bin>` instead of a bare `npx ark-check`, which npm would
331
+ resolve as a nonexistent `ark-check` package (404) or a stale global. `--doctor` leads with
332
+ `PACKAGE_PIN_ABSENT` when an existing generated file still uses the local runner without the pin.
333
+ `start --remove-host`
334
+ never runs the package manager. An explicit `--archetype` / `--preset` on `ark start` is the
335
+ contract that is written (not only a way past the shape-confidence gate), and re-running
336
+ `ark start --apply` on a project that already has `ark.config.json` keeps that contract and is not
337
+ re-gated on detected shape confidence (so the `--remove-host` restore command works).
338
+ The generated `typecheck` script and CI step are added only when `typescript` is a dependency
339
+ (or resolves from the project root); otherwise setup prints a note.
340
+
315
341
  **Day-zero origin (2.12+):** `ark init` freezes `.ark/reports/origin.*` before writing agent
316
- docs or CI templates. Compact `ark start` previews first and keeps the applied setup small
342
+ docs or CI templates (the day-zero HTML is written under `.ark/reports/`, not the project root). Compact `ark start` previews first and keeps the applied setup small
317
343
  (budget: 8 files / 32 KB including project `.mcp.json`);
318
344
  run `ark-check --report ark-report.html` explicitly when you want to establish an origin/evolution
319
345
  baseline. Do not `--reset-origin` unless the user explicitly wants a new baseline.
320
346
  `ark-check --report --no-archive` still creates `origin.*` on the first report (or on an explicit
321
347
  reset) and refreshes `latest.*`; it skips only the timestamped JSON file under
322
348
  `.ark/reports/history/`.
349
+ `--report` applies the committed `.ark-baseline.json` by default (the same freeze `--doctor`
350
+ reads), so `latest.json` and `ark status` count frozen debt as frozen, not active. Under
351
+ `--strict` / `--strict-merge` / `--contract-diff` the implicit freeze is not applied (pass
352
+ `--baseline` explicitly), so `--report` never changes a merge verdict. Only
353
+ `--report` refreshes that snapshot; a plain check or `--doctor` does not.
323
354
  Snapshots record best-effort, shell-free Git provenance (`HEAD`, attached branch, and dirty
324
355
  worktree state). Evolution keeps raw metrics visible across ArkGate upgrades, but an Ark score
325
356
  delta is comparable—and therefore rendered—only when origin and current snapshots use the same
@@ -590,6 +621,9 @@ block a promotion, and the surface names which one fired:
590
621
  - **`no-coverage-evidence`** — an invariant whose evidence does not support
591
622
  promotion. The text is `canPromoteInvariant`'s own, so this surface can never
592
623
  promise a promotion the gate then refuses.
624
+ - **`probe-survived`** — the committed invariant probe shows the covering tests
625
+ still pass with the rule broken, or never call it. Same judge; named apart
626
+ because the fix is a stronger test. See [Probe an invariant](#probe-an-invariant).
593
627
 
594
628
  It needs no TypeScript and runs no analysis: the contract, the ArkRules
595
629
  documents it points at, and the coverage evidence walk (a filesystem walk plus a
@@ -625,7 +659,9 @@ Plan by default — there is no `--dry-run` anywhere in `bin/`. `--apply` needs
625
659
  one named rule id: `--promote <ruleId> --apply`, or `--promote=<ruleId>` when
626
660
  the id starts with `-`. It refuses a bare `--promote --apply` rather than
627
661
  rewriting the contract in bulk behind a single flag, refuses an id declared in
628
- two documents rather than silently writing the first, and refuses to make a
662
+ two documents rather than silently writing the first (starter ids are unique:
663
+ `thin-persistence-adapter` / `thin-presentation-adapter`, and a layer that clones
664
+ another layer's archetype gets its ids prefixed with its kebab-cased name), and refuses to make a
629
665
  contract change on a cost this run did not measure. `--promote` cannot be
630
666
  combined with a mode that answers first (`--sensors`, `--coverage`, `--plan`,
631
667
  `--doctor`, …) — that printed the report and exited 0 having written nothing —
@@ -642,6 +678,63 @@ than a targeted text edit. Exit 0 for a preview or a successful write, 1 for an
642
678
  unknown rule id or a refused write, 2 for bad arguments or ArkRules references
643
679
  that will not load.
644
680
 
681
+ ### Probe an invariant
682
+
683
+ A test title that names an invariant counts as coverage. That proves the test
684
+ exists, not that it would fail if the rule broke. The probe checks that, on
685
+ request:
686
+
687
+ ```bash
688
+ npx arkgate-check --probe-invariants # every invariant with a coverage.symbol
689
+ npx arkgate-check --probe-invariants=INV-REFUND-WINDOW
690
+ npx arkgate-check --probe-invariants --write # save .ark/invariant-probe.json
691
+ npx arkgate-check --probe-invariants --runner vitest # only when the runner is unclear
692
+ ```
693
+
694
+ ```text
695
+ INV-REFUND-WINDOW: the tests still pass when the guard is negated (src/domain/refunds/refundPolicy.ts:14).
696
+ The test names the rule but does not pin it. Next: add a case that fails when the rule is broken, then re-run with --write.
697
+ ```
698
+
699
+ For each invariant with a `coverage.symbol`, it copies the project to a
700
+ temporary folder, then runs only the covering tests (titles that name the id,
701
+ and tests that import the symbol's file), with your own runner (vitest, jest or
702
+ `node --test`):
703
+
704
+ 1. unchanged — they must pass;
705
+ 2. with the file throwing on load — a test must fail, or the copy is not the one
706
+ the tests load (`inconclusive`);
707
+ 3. with the symbol throwing when called — a test must fail, or the tests never
708
+ call it (`not-reached`);
709
+ 4. with at most three small changes inside the symbol (negate a guard, drop a
710
+ `throw`, flip or shift a comparison, shift a constant) — each must make a test
711
+ fail, or the change went unnoticed (`survived`).
712
+
713
+ Every row reads `killed`, `survived`, `not-reached`, `inconclusive` or
714
+ `unprobeable` (no symbol, a type or interface, no covering test — a declaration
715
+ is not a test). It is not a score.
716
+
717
+ **Your files are never changed.** Every change happens in the copy, which is
718
+ removed when the run ends, including on Ctrl-C. `--write` adds only
719
+ `.ark/invariant-probe.json`. Exit 0 when nothing survived or went unreached,
720
+ 1 when something did, 2 when it could not run (`reasonCode` in the JSON).
721
+
722
+ **What the result can do.** Only refuse. When the committed file has a fresh
723
+ `survived` or `not-reached` row, `--promote` and the policy delta refuse to
724
+ promote that invariant to `enforced` (blocker `probe-survived`). Fresh means
725
+ the symbol file, every covering test and the invariant are unchanged since the
726
+ run; anything else is stale and changes nothing, like `killed`, `inconclusive`
727
+ or no file at all. Status and `--rules-inventory` show the state per invariant
728
+ (`INVARIANT_PROBE_SURVIVED` when it refuses). CI sees the refusal only if you
729
+ commit the file.
730
+
731
+ **Trust.** Running the probe runs your tests, exactly like `npm test`; you start
732
+ it, nothing else does. It never runs from the write hook, MCP, ESLint, the
733
+ GitHub Action or `--strict-merge`, and refuses to combine with them. The runner
734
+ gets a short list of environment variables (no tokens, proxies pointed at a
735
+ dead port, `HOME` and the temp folder inside the copy). That is best effort,
736
+ not a sandbox.
737
+
645
738
  ### Literal path drift after a rename (`--path-drift`)
646
739
 
647
740
  A repo path written inside a **string, a comment or a docstring** is invisible to
@@ -714,6 +807,78 @@ TS/TSX gate the type-aware passes use, because a comment is not code and the
714
807
  class was first found in a `.css` file. Generated files are skipped, and every
715
808
  file the walk refuses is counted by reason in the output.
716
809
 
810
+ ### Files nothing imports
811
+
812
+ A rewrite often lands next to the old file, and the old one stays. Nothing
813
+ imports it. Status reads that from the import edges the check already resolved,
814
+ so it costs no second analysis and never changes the check.
815
+
816
+ ```bash
817
+ npx arkgate-check --doctor # one dim count line at most
818
+ npx arkgate-check --doctor --all # the list, plus exports nothing imports by name
819
+ npx arkgate-check --doctor --json # doctor.orphanModules
820
+ ```
821
+
822
+ ```text
823
+ Nothing imports src/lib/legacy-pricing.ts, and no entry point covers it.
824
+ Next: Delete it through the write gate, or add it to .ark/entry-points.json if a framework loads it.
825
+ ```
826
+
827
+ A file is listed only when no governed file, no test, no project script and no
828
+ known entry point imports it. Entry points come from `package.json` (`main`,
829
+ `exports`, `bin`, scripts — built `dist/` paths map back to source), CI workflow
830
+ run steps, framework conventions (Next routes and `middleware`, Vite
831
+ `index.html`, Nest `main`, Vercel `api/`, Storybook stories), the arkRun /
832
+ arkOrder roots in `ark.config.json`, config and setup files, ambient declaration
833
+ files, and `.ark/entry-points.json`. A file only tests import is counted, not
834
+ listed.
835
+
836
+ When a dynamic import, `import.meta.glob`, `new URL(…, import.meta.url)`, a
837
+ quoted path, or an import that did not resolve could still reach a file, the
838
+ item says so (`maybe-dynamic`, `maybe-unresolved`) and the section is
839
+ `partial`. Read those before deleting. A framework or loader ArkGate does not
840
+ know goes in the sidecar, with a reason and an optional review date:
841
+
842
+ ```json
843
+ {
844
+ "schemaVersion": "1",
845
+ "entryPoints": [
846
+ { "glob": "src/plugins/**", "reason": "loaded by name from the plugin manifest", "reviewBy": "2027-01-01" }
847
+ ]
848
+ }
849
+ ```
850
+
851
+ Delete one file at a time through the write gate (`/ark-fix`), then run status
852
+ again. Never delete a `maybe-*` item without reading what may load it.
853
+
854
+ ### Copies across a wall
855
+
856
+ A slice wall denies the import. The cheapest way around it is to copy the code,
857
+ and a copy is not an import, so the check cannot see it. Status details can.
858
+
859
+ ```bash
860
+ npx arkgate-check --doctor --all # the list, with a place the shared code could live
861
+ npx arkgate-check --doctor --all --json # doctor.crossWallDuplication
862
+ ```
863
+
864
+ ```text
865
+ This code is copied between src/features/billing and src/features/invoices (34 of 35 names match).
866
+ The wall stops the import, not the copy. Next: move it to src/shared/ with /ark-place.
867
+ ```
868
+
869
+ A copy is listed only when its two files sit on two sides of a wall the check
870
+ would enforce — two slices, two child slices of one universe — or in two
871
+ layers. Copies inside one slice are counted, never listed: the slice may
872
+ import its own code. The suggested home comes from your config: the declared
873
+ `sharedRoots`, the universe common folder (`childSlices.commonFolders`), or the
874
+ lower layer both sides may import. When none fits, it says `/ark-place`.
875
+
876
+ Compact status never runs this; its JSON says `not-run` and names the command.
877
+ Tests, `.d.ts`, generated files and files over 256 KB are never read for it. The
878
+ thresholds are fixed (at least 50 tokens and 5 lines, and most names matching);
879
+ there is no config key. Move one copy at a time through the write gate
880
+ (`/ark-fix`), import it from both sides, then run status again.
881
+
717
882
  ### Presets
718
883
 
719
884
  - `hexagonal` / `layered` / `feature-sliced` / `monorepo` / **`ui-surface`** (UI/Vite/Remotion-style) / **`vertical-slice`** (features/* + peerIsolation) / **`ddd-bounded-contexts`** (contexts/*/domain|application|infra + shared kernel)
@@ -1005,6 +1170,13 @@ a `Domain.*` event — the publish throws `ObservedLayerFlowViolationError` befo
1005
1170
  reaches history, outbox, or subscribers. Use `'soft'` to record `layer.observedViolation`
1006
1171
  trace/audit records without blocking, or `'off'` to disable. Agents should name the event's
1007
1172
  `source` honestly: it is checked against the layer matrix, not just the intent name.
1173
+ Kernels built from `ark.config.json` map intents to layers through `layers[].intentPrefixes`;
1174
+ canonical layer names (`DomainModel`, `ApplicationOrchestration`, … as `ark init` writes them)
1175
+ without prefixes get the built-in ones. A custom-named deny-rule layer with no prefixes is
1176
+ recorded once as a `layer.observedFlowUnresolvable` audit record
1177
+ (`ARKRUN_LAYER_FLOW_UNRESOLVABLE`) and listed by `ark doctor`; pass
1178
+ `enforceObservedLayerFlow: 'hard'` explicitly to make it throw `ArkKernelConfigError`.
1179
+ `peerIsolation` slice walls are not evaluated at runtime (names cannot place a slice).
1008
1180
 
1009
1181
  Strict kernels also require published events to have a registered source intent
1010
1182
  and a matching event contract:
@@ -1150,9 +1322,9 @@ edges are not denied by that rule.
1150
1322
  - **Denied:** `src/features/auth/**` → `src/features/payments/**` (different slice id).
1151
1323
  - **Allowed:** same-slice imports when both paths classify; classic non-peerIsolation denies still apply across layers.
1152
1324
  - **`sliceFolders`:** optional parent segments (default: inferred from layer globs). A bare name stays an unanchored one-segment match (`features` on `src/features/auth` is `features/auth`). A starred anchored pattern with `sliceIdentity` `path` (the default) includes the literal prefix plus the star bindings (`lib/features/*/*` → `lib/features/projects/rfi`), so parallel trees get different ids.
1153
- - **`sliceIdentity`:** `path` (default, also when the key is absent) or `stars`. `path` keeps today's slice ids. `stars` names a starred prefix as the last literal plus the star bindings (`lib/features/*/*` and `lib/repositories/features/*/*` both yield `features/projects/rfi`). Doctor names both prefixes when they collapse onto one id. Additive — no migration.
1325
+ - **`sliceIdentity`:** `path` (default, also when the key is absent) or `stars`. `path` keeps today's slice ids. `stars` names a starred prefix as the last literal plus every star binding (`lib/features/*/*` and `lib/repositories/features/*/*` both yield `features/projects/rfi`; `modules/*/api/*` keeps the module: `orders/api/v1`). Doctor names both prefixes when they collapse onto one id. Additive — no migration.
1154
1326
  - **Fail-closed:** missing paths, empty/unresolvable slice folders, or unclassifiable either side → **deny** via peerIsolation (cannot prove same-slice).
1155
- - **`childSlices`** (optional): a second wall under the same rule. The universe wall runs first and is unchanged when the block is absent. With it present, a cross-universe deny is `reasonId` `CROSS_PARENT_SLICE` (never advisory). Inside one universe, `commonFolders` and flat files are common; a child may import them; common may import a child only when `parentMayImportChild` is true. Sibling crossings are `CROSS_SIBLING_SLICE` — error when `siblings` is `deny` (the default), warning when `advisory`, and a new one past the baseline still fails. `ruleId` stays `LAYER_IMPORT_VIOLATION`. arkgate 4.8.22 and older reject the key.
1327
+ - **`childSlices`** (optional): a second wall under the same rule. The universe wall runs first and is unchanged when the block is absent. With it present, a cross-universe deny is `reasonId` `CROSS_PARENT_SLICE` (never advisory). Needs `peerIsolation: true` and `allowed: false` on the rule. Inside one universe, flat files and a `commonFolders` directory directly under the universe are common (the same folder name inside a child belongs to the child); a child may import them; common may import a child only when `parentMayImportChild` is true. Sibling crossings are `CROSS_SIBLING_SLICE` — error when `siblings` is `deny` (the default), warning when `advisory`. With a baseline, a new advisory crossing fails only when the baseline already records an advisory crossing of that rule, or with `siblings.ratchet: true`; `ratchet: false` measures only. `childSlices.message` is the inner-wall text; the rule `message` stays the universe-wall text. `ruleId` stays `LAYER_IMPORT_VIOLATION`; adapter diagnostics carry `evidence.reasonId`. arkgate 4.8.22 and older reject the key; `CONFIG_CHILD_SLICES_VERSION` fires only when the repo pins such a version (package.json, hook, CI), at that file.
1156
1328
  - **`sharedRoots`** (4.8.4): roots the repo declares shared on purpose (`["ui", "hooks", "lib/permissions"]`). A file under a declared shared root is evidence, not an unclassifiable path, so fail-closed stops firing on every shared file. **Anchored** — the root starts the path, optionally after one `src/` or `app/`; write deeper or monorepo roots out (`packages/web/src/ui`) or glob them, and a bare `*` / `**` is refused. A path that still resolves to a slice keeps its slice.
1157
1329
  - **`allowedCrossSlice`** (4.8.4): `[{ "from": "features/checkout", "to": "features/catalog" }]` — one directed slice→slice edge the repo declares on purpose. The reverse still denies.
1158
1330
  - **The denial names its reason:** `cross-slice edge a → b` (a fact about the code) vs `unclassifiable path (…)`, `no slice folders`, `no path evidence` (facts about the evidence ArkGate had).
@@ -1241,8 +1413,14 @@ imports/exports; resolved CLI/preflight is authoritative outside that envelope),
1241
1413
  `ark/no-denied-capabilities` (per-layer capability deny sets),
1242
1414
  `ark/no-arkrun-kernel-in-domain` / `ark/no-arkrun-direct-new` /
1243
1415
  `ark/no-arkrun-transport-bypass` (ArkRun extra; silent when absent; import / `new`
1244
- envelope only), `ark/no-raw-event-publish`, and
1245
- `ark/require-publish-source`. See [ai-gates.md](ai-gates.md).
1416
+ envelope only), `ark/no-arkorder-kernel-in-domain` / `ark/no-arkorder-generic-update`
1417
+ (ArkOrder extra), `ark/arkrules-structure` (ArkRules structure sensors, file-local),
1418
+ `ark/no-raw-event-publish`, and `ark/require-publish-source`. Findings ark-check reports as
1419
+ warnings (type-only placement debt, advisory slice walls, advisory extras) report on the
1420
+ warn-level `ark/architecture-advisory`, so ESLint errors only where ark-check fails (a
1421
+ config without that rule gets them on the blocking rule id, tagged advisory). An
1422
+ invalid contract is one `configInvalid` error per file, not a crash. See
1423
+ [ai-gates.md](ai-gates.md#eslint-editor-feedback--bounded-parity-envelope).
1246
1424
 
1247
1425
  ## Runtime Observability
1248
1426
 
@@ -1324,7 +1502,10 @@ not replace your web framework, HTTP clients, or job scheduler.
1324
1502
  The strongest place to constrain an AI agent is the moment it writes a file, not after.
1325
1503
  `arkgate-mcp` / `ark-mcp` exposes ArkGate over MCP (JSON-RPC over stdio; it prefers a usable
1326
1504
  project TypeScript API, then exact `typescript-ark-host@6.0.3`) so a host can gate
1327
- the write path:
1505
+ the write path. `arkgate mcp` / `ark mcp` starts the same server; that is the form
1506
+ `npx arkgate@<version> mcp --root .` and the MCP Registry use, since npx runs the bin named
1507
+ after the package. Without `--config` the server reads `ark.config.json` when it exists and still
1508
+ starts in a fresh project; an explicit `--config` that points at a missing file exits 1:
1328
1509
 
1329
1510
  For a complete multi-file candidate, use `ark preflight --changes changes.json --json` or MCP
1330
1511
  `ark_prepare_change`. Add `--change-map map.json` (or MCP `changeMap`) only for an explicit schema
@@ -1370,13 +1551,13 @@ The server exposes these thirteen tools. Every tool accepts the additive
1370
1551
  |------|---------------------------|
1371
1552
  | `ark_identity` | `{ project: { expectedRoot, expectedProjectId? } }`: return the canonical root/config, stable project id, contract identity, and live runtime identity; use it before every other project-bound surface. |
1372
1553
  | `ark_manifest` | No non-project args: return the machine-readable architecture contract with an authoritative binding after the identity handshake. |
1373
- | `validate_code` | `{ source, layer?, filePath? }`: validate one snippet; infer the layer from `filePath` when possible; return an error result when invalid. |
1554
+ | `validate_code` | `{ source, layer?, filePath? }`: single-file lexical check (layer plane plus the enforced ArkRun / ArkRules structure / ArkOrder sensors CI runs on that file); infer the layer from `filePath` when possible. Always partial: `valid:false` / `isError:true` until complete-candidate preflight — read `lexicalValid` for the one-file verdict. Not a hook; hard blocking is `arkgate-mcp --hook`. |
1374
1555
  | `ark_check` | `{ strict?, baseline? }`: run the full project architecture check. `verdict` separates `identity`, `completeness`, `graph`, `coverage`, `gates`, and `overallOk`; no individual green fact substitutes for the combined verdict. |
1375
- | `ark_policy_delta` | `{ baseConfig, candidateConfig?, acknowledgement? }`: classify a complete contract transition; never edits the contract. Weakening / new layer / new allow edge needs `adrPath` on the acknowledgement. |
1556
+ | `ark_policy_delta` | `{ baseConfig, candidateConfig?, baseArkRuleFiles?, candidateArkRuleFiles?, acknowledgement? }`: classify a complete contract transition, ArkRules included; never edits the contract. `baseArkRuleFiles` (`{ "<path from baseConfig.arkRules>": <file JSON> }`) is required when `baseConfig` maps `arkRules`. Include each `childSlices.arkRulesFile` path in that map when the wall discovers one. Weakening / new layer / new allow edge needs `adrPath` on the acknowledgement. |
1376
1557
  | `ark_coverage` | No args: report per-layer counts, every unclassified file, unmatched layers, and missing rule edges. |
1377
- | `ark_place` | `{ filePath?, description? }`: resolve or propose a governed home and return its import/global constraints. |
1378
- | `ark_prepare_write` | `{ source, filePath?, description?, layer? }`: compose placement and snippet validation, with hashes and a mechanical-safe patch when available. |
1379
- | `ark_prepare_change` | `{ changes, changeMap? }`: preflight one complete create/update/delete batch in memory; never writes files. |
1558
+ | `ark_place` | `{ filePath, description? }`: resolve the governed home for `filePath` (fail-closed without it; never invents a path) and return its import/global constraints. |
1559
+ | `ark_prepare_write` | `{ source, filePath, description?, layer? }`: compose placement and snippet validation, with hashes and a mechanical-safe patch when available. `filePath` is required (fail-closed). Partial like `validate_code`: read `lexicalValid`. |
1560
+ | `ark_prepare_change` | `{ changes, changeMap? }`: preflight one complete create/update/delete batch in memory against the same Effective Contract as `ark-check` (ArkRules included: same `policyHash`, structure/invariant findings; a missing referenced ArkRules file fails closed); ArkRules-plane findings the base tree already has are returned as `preExisting: true` warnings and do not block, so baselined brownfield debt does not reject an unrelated batch; never writes files. |
1380
1561
  | `ark_recommend` | No args: return the deterministic application-shape plan used by `ark-check --recommend --json`. |
1381
1562
  | `ark_suggest_include` | No args: propose TypeScript/JavaScript include roots from workspaces and nested packages. |
1382
1563
  | `ark_rules_inventory` | No args: inventory possible intra-layer rules using configured layer evidence when available; test/fixture/seed/migration surfaces and narrow technical constants are excluded from extraction pilots. Counts are not a score. |
@@ -1406,7 +1587,13 @@ Every project-bound tool success, tool error, and JSON-RPC error data carries:
1406
1587
  ```
1407
1588
 
1408
1589
  `projectId` stays stable across process restarts and contract edits; `runtimeId` and
1409
- `processStartedAt` identify this live process. Binding states are:
1590
+ `processStartedAt` identify this live process. The contract (`ark.config.json`, the project
1591
+ manifest, referenced ArkRules files) is loaded once at startup: after any of them changes on
1592
+ disk, every project tool except `ark_identity` returns `CONTRACT_STALE` (`isError`,
1593
+ `authoritative: false`, `contractStale: true`) until the MCP server restarts — the process never
1594
+ answers authoritatively from an old contract. `ark_identity` keeps answering with
1595
+ `contractStale: true` so the host can diagnose it; the project-local CLI reads the current
1596
+ contract meanwhile. Binding states are:
1410
1597
 
1411
1598
  - `matched` — canonical `expectedRoot` is the exact project root, or it is a contained
1412
1599
  descendant and the caller also supplied the matching project id; `authoritative` is `true`;
@@ -1513,8 +1700,22 @@ doctor → compact router (and `/ark-autopilot` only after the skill pack).
1513
1700
  | `GET /workflows` | Workflows monitor: counts + `workflows[]` summaries (`id`, `name`, `status`, optional `currentStep` / `error`) |
1514
1701
 
1515
1702
  Dual package bins **`ark-dashboard`** and **`arkgate-dashboard`**
1516
- (`bin/ark-dashboard.mjs`) poll `--url` (default `http://127.0.0.1:3000/snapshot`)
1517
- and sibling `/outbox` + `/workflows` on an interval (`--interval`, 200–60000 ms).
1703
+ (`bin/ark-dashboard.mjs`) poll `--url` and sibling `/outbox` + `/workflows` on an
1704
+ interval (`--interval` / `--timeout`, clamped to 200–60000 ms). `--url` accepts the
1705
+ inspector root (`handle.url`) or `handle.snapshotUrl`. `startInspector()` binds a
1706
+ **random port** unless you pass `{ port }`, so pass the URL your app printed (or set
1707
+ `ARK_DASHBOARD_URL`); the fallback `http://127.0.0.1:3000/snapshot` only matches
1708
+ `startInspector({ port: 3000 })`, and the dashboard prints that hint when it cannot
1709
+ connect. `--once` renders one frame and exits (non-zero when unreachable);
1710
+ `--help` / `--version` exit 0; unknown flags exit 2 with one line.
1711
+
1712
+ The default in-memory event buffer (`InMemoryEventBuffer`) is capped at
1713
+ `maxHistorySize` (default 1000) and its records are marked `dispatched` once the kernel
1714
+ finishes local delivery, so `/outbox` shows real backlog only. Broker sends are marked
1715
+ `dispatched` / `failed` from the handoff outcome, and a failed handoff (awaited or
1716
+ fire-and-forget) records an `event.handoffFailed` trace + audit entry. An injected
1717
+ `eventBuffer` stays relay-owned: records remain `pending` for your relay's `claim` /
1718
+ `markDispatched` (except broker handoffs the kernel performs itself).
1518
1719
  ANSI escape sequences + polling only — no React, Ink, or Blessed. Use
1519
1720
  `ark dashboard` / `arkgate dashboard` (passthrough to `bin/ark-dashboard.mjs`) or the
1520
1721
  dual bins `ark-dashboard` / `arkgate-dashboard`.
package/docs/ai-gates.md CHANGED
@@ -50,7 +50,8 @@ fail-open. OpenCode is MCP-only. Required CI is always the merge line.
50
50
 
51
51
  Everything below uses the same `ark.config.json` as `arkgate-check` / `ark-check` (CI) — one
52
52
  rules file shared by every surface. From **4.0**, optional **ArkRules** (`arkRules` map +
53
- `arkrules/*.json`) ride the **same** write path, doctor, and CI adapter; absence of ArkRules
53
+ `arkrules/*.json`) ride the **same** write path, doctor, and CI adapter (ESLint runs the
54
+ file-local structure sensors only — see [ESLint](#eslint-editor-feedback--bounded-parity-envelope)); absence of ArkRules
54
55
  does not change inter-layer verdicts. Optional ArkRun / ArkOrder extras ride that envelope
55
56
  when on (`proposeRelease` then `apply`). Absence is silent. Label residual **`[Layer]`** vs
56
57
  **`[ArkRules]`** vs **`[ArkRun]`** vs **`[ArkOrder]`**. See
@@ -128,7 +129,11 @@ project id before using project evidence.
128
129
  Like `ark-check --baseline`, the hook ratchets: an edit is blocked only when it **adds**
129
130
  violations relative to the file's current on-disk state, so files with pre-existing
130
131
  (baselined) violations stay editable — they just can't get worse. New files block on
131
- every violation.
132
+ every violation. A contract failure (`WRITE_GATE_UNAVAILABLE`, e.g. a referenced ArkRules
133
+ file missing) is never ratcheted: it blocks edits to existing files as well as new ones.
134
+ The Codex `apply_patch` atomic preflight ratchets ArkRules-plane findings (`ARKRULE_*`,
135
+ `INVARIANT_*`) the same way against the base tree: debt the repository already has is
136
+ reported as a warning (`preExisting: true`), and only findings the patch adds block.
132
137
 
133
138
  The hook classifies import targets with the same `layerForRelativePath` specificity as
134
139
  `ark-check` (an explicit file pattern beats a broader glob such as `src/lib/**`).
@@ -250,7 +255,11 @@ After edits run: npx ark-check --root . --config ark.config.json --strict
250
255
  ```
251
256
 
252
257
  The hook belongs in the **project's** `.claude/settings.json` (that's what
253
- `--install-agent-gates` generates). It is also safe by construction if you prefer it in
258
+ `--install-agent-gates` generates). An existing `.claude/settings.json` (or `.codex/hooks.json`)
259
+ is **merged**, not skipped or replaced: Ark upserts only its own `SessionStart` / `PreToolUse`
260
+ `arkgate-mcp` entries and keeps `permissions`, `env`, other hook events, and your own hook
261
+ entries; re-running is a no-op. Only a file that is not a JSON object is left untouched (and
262
+ `--require-write-hook` then refuses until you fix it or pass `--force`). It is also safe by construction if you prefer it in
254
263
  your global settings: without an `ark.config.json` in the project, `--session-context`
255
264
  prints nothing and exits 0, so non-Ark projects are untouched.
256
265
 
@@ -721,8 +730,25 @@ If your runtime can run a shell command before file writes and pass the tool pay
721
730
  - Grok: `{ "decision": "deny", "reason": "…" }` on stdout when blocked
722
731
  - Antigravity: **stdout `decision` is required** — allow → `{ "decision": "allow" }`; deny →
723
732
  `{ "decision": "deny", "reason": "…" }` (exit 2 still set on deny)
724
- - plumbing problems (no stdin, non-source files, files outside `--root`) never block; Antigravity
725
- still emits `{ "decision": "allow" }` on those fail-open paths
733
+ - plumbing problems (no stdin, malformed payload, non-file tools, non-source files, files outside
734
+ `--root`) never block; Antigravity still emits `{ "decision": "allow" }` on those fail-open paths.
735
+ "Outside `--root`" is judged after realpath: `/tmp` vs `/private/tmp` or a symlinked checkout is
736
+ the same workspace and is gated
737
+ - a gate that **cannot run** blocks every governed source write (exit `2`, `WRITE_GATE_UNAVAILABLE`,
738
+ plus the host deny JSON): `ark.config.json` missing / unparsable / invalid, a referenced ArkRules
739
+ file missing or invalid, or `dist/` missing or broken. No checker, no write. This applies to
740
+ existing files too: the same-file ratchet never cancels a contract failure. Writes to the law
741
+ files (`ark.config.json` and every ArkRules file it references under `arkRules`) are still
742
+ allowed so the contract can be repaired, but a write or `apply_patch` delete that would leave
743
+ one unloadable is denied (`WRITE_GATE_UNAVAILABLE`)
744
+ - Codex `apply_patch`: the full grammar is reconstructed (`*** End of File`, a first hunk without
745
+ `@@`, `*** Move to:` judged at the destination). A patch that truly cannot be reconstructed is
746
+ allowed with a stderr notice and stays CI-backed
747
+ - the hook runs the same file-local extra planes CI runs on that file: enforced ArkRun editor
748
+ sensors, enforced ArkRules structure sensors (`ARKRULE_STRUCTURE`), and enforced ArkOrder editor
749
+ sensors (`ARKORDER_KERNEL_IN_DOMAIN`, `ARKORDER_GENERIC_UPDATE`, …), under the same
750
+ classification floor. Cross-file evidence (invariant coverage, missing plane roots) stays
751
+ CI-authoritative
726
752
 
727
753
  ## ESLint (editor feedback) — bounded parity envelope
728
754
 
@@ -731,31 +757,108 @@ Layer imports and purity globals are driven by **`ark.config.json`** (walk-up fr
731
757
  linted file).
732
758
 
733
759
  ```js
734
- // eslint.config.js (flat config)
760
+ // eslint.config.js (flat config, ESM)
735
761
  import ark from 'arkgate/eslint';
736
762
 
737
763
  export default [
738
764
  ark.configs.recommended,
739
- // no-domain-infra-imports → config-driven layer edges (type-only + value)
740
- // no-forbidden-globals → layer.forbiddenGlobals from ark.config.json
741
- // ark/no-denied-capabilities → layer.capabilities.deny / layer.pure
742
- // ark/no-arkrun-kernel-in-domain + no-arkrun-direct-new + no-arkrun-transport-bypass
743
- // → arkRun extra (silent when absent; import / `new` envelope only)
744
- // no-raw-event-publish + require-publish-source → runtime event hygiene
765
+ // error: no-domain-infra-imports → config-driven layer edges that fail ark-check
766
+ // error: no-forbidden-globals → layer.forbiddenGlobals from ark.config.json
767
+ // error: no-denied-capabilities → layer.capabilities.deny / layer.pure
768
+ // error: no-arkrun-kernel-in-domain + no-arkrun-direct-new + no-arkrun-transport-bypass
769
+ // → arkRun extra (silent when absent; import / `new` envelope only)
770
+ // error: no-arkorder-kernel-in-domain + no-arkorder-generic-update → arkOrder extra
771
+ // error: arkrules-structure → ArkRules structure sensors (file-local)
772
+ // error: no-raw-event-publish + require-publish-source → runtime event hygiene
773
+ // warn: architecture-advisory → the findings ark-check reports as warnings
745
774
  ];
746
775
  ```
747
776
 
748
- **Exact layer-edge parity envelope:** the linted production source is on disk, inside
749
- `include`, outside configured/generated exclusions, parse-clean, and uses a static
750
- `import`/`export` with a **relative** or **tsconfig `paths` / `baseUrl` alias** (e.g. `@/*`)
751
- literal whose target is also on disk. Inside that envelope, ESLint uses the same layer glob
752
- specificity, rule decision, rule id, and evidence as the resolved CLI. It reloads
753
- `ark.config.json` when its content changes and never invents a not-yet-created target.
777
+ ```js
778
+ // eslint.config.cjs (CommonJS works the same way)
779
+ const ark = require('arkgate/eslint');
780
+ module.exports = [ark.configs.recommended];
781
+ ```
754
782
 
755
- **Path-alias residual (honest):** simple `paths` + `baseUrl` (including one level of relative
756
- `extends`) are resolved. Not claimed: TypeScript project references, multi-target path arrays
757
- beyond the first entry, catch-all `*` mappings, package/workspace bare imports, or symlink
758
- hops. Those stay CI / preflight / write-gate truth.
783
+ `require('arkgate/eslint')` returns the plugin itself (`configs`, `rules`, `meta`), the same
784
+ object as the ESM default export.
785
+
786
+ **Severity follows ark-check, per finding.** ESLint takes severity from the rule's configured
787
+ level; one rule cannot report some findings as errors and others as warnings. So the plugin
788
+ splits them by rule id. The blocking rules above report only findings that fail ark-check
789
+ (`severity: "error"`). Every finding ark-check reports as a warning (`failsStrict: false`)
790
+ reports on **`ark/architecture-advisory`** instead, which `recommended` sets to `warn`:
791
+
792
+ - type-only placement debt (a non-peer `import type` across a denied edge);
793
+ - a `CROSS_SIBLING_SLICE` crossing under `childSlices.siblings: "advisory"` (or not listed in
794
+ `enforce`);
795
+ - `arkRun.mode`, `arkOrder.mode`, or an ArkRules rule set to `advisory`.
796
+
797
+ So `eslint` exits `0` wherever `arkgate-check` exits `0` for these findings, and a Next.js
798
+ build that runs ESLint does not fail on them. The warning text names the ark-check rule id.
799
+ The blocking rule and `architecture-advisory` share one evaluation per file, so enabling both
800
+ does not resolve a file's imports twice.
801
+
802
+ A hand-written config that turns on single rules without `architecture-advisory` does not
803
+ lose these findings. They fall back to the blocking rule id, at that rule's level, with the
804
+ text `[<ark-check rule id>; advisory — does not fail ark-check. Enable
805
+ ark/architecture-advisory …]`. Add `'ark/architecture-advisory': 'warn'` to see them as
806
+ warnings instead.
807
+ There is no baseline in the editor: a new advisory crossing past the recorded baseline, which
808
+ fails in CI, is still only a warning here.
809
+
810
+ **Messages are the ark-check text.** A layer denial reads
811
+ `Architecture: <ark-check message> Specifier: <import>`. That message carries the rule-level
812
+ `message` and the slice reason (cross-slice, cross-sibling, cross-parent, unclassifiable) plus
813
+ the type-only suffix. ArkOrder and ArkRules findings print the sensor's own message.
814
+
815
+ **Invalid contract.** When `ark.config.json` does not parse or validate, or a referenced
816
+ ArkRules file is missing or invalid, ESLint does not crash. The file gets one
817
+ `configInvalid` error carrying the validator text (fail closed, same as `ark-check`, which
818
+ exits `2`), and other rules and files keep linting.
819
+
820
+ **`eslint --cache`.** The plugin has `meta` (`arkgate@<version>`), so an upgrade invalidates
821
+ cached results. `configs.recommended` also carries `settings.ark.contractHash`, a fingerprint of
822
+ the `ark.config.json` (and referenced ArkRules files) found by walking up from the **current
823
+ working directory** of the process that reads `configs.recommended` — not from the config
824
+ file's directory. Editing that contract therefore invalidates the cache too. When ESLint runs
825
+ from another directory (`eslint -c some/dir/eslint.config.js` from elsewhere, or an editor
826
+ whose cwd is a workspace root above the project), the fingerprint covers a different contract
827
+ or none. Pin it to the config file's directory yourself:
828
+
829
+ ```js
830
+ // eslint.config.js
831
+ import ark, { contractFingerprint } from 'arkgate/eslint';
832
+ const contractHash = contractFingerprint(import.meta.dirname);
833
+ export default [
834
+ ark.configs.recommended,
835
+ ...(contractHash ? [{ settings: { ark: { contractHash } } }] : []),
836
+ ];
837
+ ```
838
+
839
+ Not covered by the fingerprint: a second `ark.config.json` deeper in a monorepo,
840
+ `tsconfig.json` edits, and a cross-file target that appears or disappears. Use
841
+ `--cache-strategy content`, clear the cache, or rely on `ark-check` / CI, which stay the
842
+ source of truth.
843
+
844
+ **Exact layer-edge parity envelope:** the linted production source is inside `include`,
845
+ outside configured/generated exclusions, parse-clean, and uses a static `import`/`export` with
846
+ a **relative** or **tsconfig `paths` / `baseUrl` alias** (e.g. `@/*`) literal whose target is
847
+ on disk. Inside that envelope, ESLint uses the same layer glob specificity, rule decision,
848
+ severity, rule id, message, and evidence as the resolved CLI. It reloads `ark.config.json`
849
+ when its content changes and never invents a not-yet-created target. File-local facts
850
+ (`new`, ArkOrder, ArkRules structure) come from the editor buffer, so unsaved edits and
851
+ `--stdin --stdin-filename` are linted as written.
852
+
853
+ **Path-alias resolution:** like the CLI (`ts.findConfigFile`), the tsconfig is the one nearest
854
+ to the linted file, so a nested `apps/web/tsconfig.json` is used under a single root
855
+ `ark.config.json`. It is read as JSONC (comments, trailing commas, and `"@/*"` / `"**/*.ts"`
856
+ strings are safe). `extends` may be relative, absolute, a package (`@tsconfig/next/...`), or an
857
+ array. Inheritance follows TypeScript: a child's `paths` replaces the parent's, and `paths`
858
+ resolve against `baseUrl`, else against the tsconfig that declares them. Each target in a path
859
+ array is tried in order, and a `*` may sit mid-pattern. Parsed tsconfigs are cached and re-read
860
+ when the file changes. Not claimed: TypeScript project references, catch-all `*` mappings,
861
+ package/workspace bare imports, or symlink hops. Those stay CI / preflight / write-gate truth.
759
862
 
760
863
  Outside that envelope—packages/workspaces, symlinks, CommonJS, `import = require`, dynamic
761
864
  imports, virtual creates/deletes, unresolved targets, or complete cross-file candidates—ESLint
@@ -765,8 +868,9 @@ ApplyPatch hook, or final strict CI; those paths consume the canonical resolved
765
868
  Additional rule notes:
766
869
 
767
870
  - Relative and tsconfig-aliased imports resolve only to existing on-disk TS/JS targets; package bare imports are left to CI/TS.
768
- - Value forbidden edges error (same pass/fail as `arkgate-check`). Type-only forbidden edges are **placement debt** (reported with `typeOnly`); merge blocking prefers value edges — align with doctor `typeEdgePolicy`.
769
- - `no-forbidden-globals` applies from the file layer’s `forbiddenGlobals`; the `globals` option is only a standalone fallback when no project config applies, never an override that weakens the project contract. Layers without either surface are not inventively restricted. `process` also owns exact value imports of `process` / `node:process`; type-only forms, subpaths, and `child_process` stay excluded. If the same layer also denies the `process` capability, this rule is the single `FORBIDDEN_GLOBAL` voice.
871
+ - Value forbidden edges error (same pass/fail as `arkgate-check`). Type-only forbidden edges that are not `peerIsolation` walls are **placement debt**: a warning on `ark/architecture-advisory`, never an error — align with doctor `typeEdgePolicy`.
872
+ - `no-forbidden-globals` applies from the file layer’s `forbiddenGlobals`; the `globals` option is only a standalone fallback when no project config applies, never an override that weakens the project contract. Layers without either surface are not inventively restricted. It sees dotted access (`Date.now`), static bracket access (`Date['now']`, ``Date[`now`]``, `globalThis['fetch']`), and destructuring from a global (`const { now } = Date`, `const { now: n } = globalThis.Date`); a dynamic key (`Date[k]`) is not a verdict. `process` also owns exact value imports of `process` / `node:process`; type-only forms, subpaths, and `child_process` stay excluded. If the same layer also denies the `process` capability, this rule is the single `FORBIDDEN_GLOBAL` voice.
873
+ - `require-publish-source` uses the ark-check candidate test: the first argument is an Ark intent string (`'Domain.Order.Placed'`), an `{ intent }` object, or an intent-creator reference (`OrderPlaced`, `Events.OrderPlaced`). `source` counts in the first argument's `metadata`, or in the second or third argument. A generic `pubsub.publish('USER_CREATED', …)` or `client.publish({ topic })` is not flagged. The rule is silent without an `ark.config.json` and outside `include`, like ark-check.
770
874
  - Without `ark.config.json`, `no-domain-infra-imports` emits no contract verdict.
771
875
  - **ArkRun (RN06):** when `arkRun` is present, `no-arkrun-kernel-in-domain`,
772
876
  `no-arkrun-direct-new`, and `no-arkrun-transport-bypass` reuse the same
@@ -779,6 +883,21 @@ Additional rule notes:
779
883
  kernel imports (including type-only). Type-only broker imports do not flag
780
884
  transport-bypass. Missing-root and undeclared emit/handle/depend are **not**
781
885
  in this adapter — use CLI / preflight / CI.
886
+ - **ArkOrder:** when `arkOrder` is present, `no-arkorder-kernel-in-domain` flags an
887
+ `arkgate/order` import, export-from, `require`, or dynamic import in a Domain-role layer — a
888
+ layer named `Domain` / `DomainModel` / `Domain…`, or one whose `intentPrefixes` include
889
+ `Domain.` — with the same sensor as ark-check. `no-arkorder-generic-update` runs the
890
+ file-local ξ sensors: generic `update()`, ξ field writes, `ingest()` into a Release, and
891
+ oversized `release()`. Missing-plane stays CLI / preflight / CI.
892
+ - **ArkRules:** `arkrules-structure` resolves the same Effective Contract as ark-check (a
893
+ missing or invalid referenced file is the `configInvalid` error above) and runs the
894
+ **structure** sensors on the linted file: `aggregate-private-state`,
895
+ `always-valid-factory`, `domain-event-on-mutation`, `no-anemic-model` (always advisory),
896
+ and the text heuristics for `orchestration-only`, `thin-adapter`, and
897
+ `writes-via-aggregate`. Not in the ESLint adapter — use preflight, `ark_prepare_change`, or
898
+ CI: invariant coverage and `INVARIANT_*` findings (they need test globs across files),
899
+ empty `appliesTo` and empty invariant catalogs, the structural-hint budget, and persistence
900
+ hints that need resolved import layers.
782
901
 
783
902
  Rule ids are `ark/<kebab-name>`. Individual rules are also on `ark.rules` if you wire them by hand.
784
903
  Prefer keeping editor + CI on the same `ark.config.json`. Use the rule-local `globals` list only
@@ -822,10 +941,27 @@ but stays advisory.
822
941
  **command**. The hard merge boundary is making that job a **required GitHub status context** —
823
942
  not “workflow file present.”
824
943
 
825
- Or use the repository's composite Action at a pinned release or commit:
944
+ **Skipped jobs and `needs:`:** GitHub reports a skipped job as a status that satisfies branch
945
+ protection. Doctor therefore counts an Ark job as fail-closed only when nothing can skip it on
946
+ purpose:
947
+
948
+ | Ark job shape | Workflow evidence | Required-status verdict (`ARK_DOCTOR_GITHUB=1`) |
949
+ |---------------|-------------------|--------------------------------------------------|
950
+ | No `if:`, or `if: true` / `always()` | fail-closed | required when its context is required |
951
+ | `needs:` jobs with no conditional `if:` anywhere up the chain | fail-closed (it only skips when a needed job fails) | required only when every needed job is also required; otherwise `unverified` plus gap `enforcement-ark-check-upstream-not-required` |
952
+ | Job-level `if:` on a profile output (or any other condition), or a conditional job in its `needs:` chain | not fail-closed (the skip reports success) | not counted |
953
+ | `continue-on-error: true` on the job or the Ark step | not fail-closed | not counted |
954
+
955
+ A required check bound to the GitHub Actions app counts. A check bound to another app stays
956
+ `unverified`. Doctor queries branch protection only with `ARK_DOCTOR_GITHUB=1` and a working `gh`.
957
+
958
+ Or use the repository's composite Action at a pinned release or commit. Pin the tag (or commit
959
+ SHA) that matches the `arkgate` version in your `package.json` and write hook. The Action runs the
960
+ checker from the ref you pin, so an older ref runs an older checker, and that checker rejects newer
961
+ config keys (for example `schemaVersion` 1.3, `childSlices`, `sliceIdentity`) with exit 2:
826
962
 
827
963
  ```yaml
828
- - uses: pedroknigge/arkgate@v3.7.0
964
+ - uses: pedroknigge/arkgate@v4.8.25 # same version as the arkgate devDependency
829
965
  with:
830
966
  root: .
831
967
  config: ark.config.json
@@ -912,6 +1048,18 @@ npx ark-check --update-baseline # writes .ark-baseline.json — commit it
912
1048
  npx ark-check --baseline # only NEW violations fail
913
1049
  ```
914
1050
 
1051
+ `--update-baseline` always measures the whole governed tree. It refuses `--changed` /
1052
+ `--local` and ignores `ARK_CHECK_LOCAL=1`, because a changed-files scan would overwrite the
1053
+ freeze with only that subset.
1054
+
1055
+ `ark-check --report` applies the committed `.ark-baseline.json` by default (the same file
1056
+ `--doctor` reads), so the report, `.ark/reports/latest.json`, and `ark status` agree with
1057
+ doctor on what is active versus frozen. A plain `ark-check` without `--baseline` is
1058
+ unchanged, and so is a merge verdict: with `--strict` / `--strict-merge` / `--contract-diff`,
1059
+ `--report` does **not** apply the implicit freeze (pass `--baseline` explicitly), so adding a
1060
+ reporting flag can never turn a failing merge run green. Only `--report` refreshes the `ark status` last-check snapshot; a plain check or
1061
+ `--doctor` does not write it.
1062
+
915
1063
  `--update-baseline` also patches existing check **invocations** so CI keeps using the
916
1064
  new freeze file: `package.json` scripts and GitHub workflow `run:` lines that already
917
1065
  call `ark-check` / `arkgate-check` via `npx`, `pnpm`, `yarn`, `npm`, or `node`. Those