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/arkorder.md CHANGED
@@ -58,8 +58,8 @@ They already exist:
58
58
  | Escalation as a first-class concept | `IngestResult` residual `absorb \| escalate_up \| hold` (`reasonCode`, `IngestEscalate.target`) | 4.8.0 / 4.8.6 |
59
59
  | Projection at the boundary | `Projector = (release, sigma) => Projection` | 4.8.0 |
60
60
  | Cap on badly designed slow parameters | `DEFAULT_MAX_XI_KEYS = 7`, `maxXiKeys`, `ARKORDER_TOO_MANY_PARAMS`, `ARKORDER_EMPTY_XI`, `ARKORDER_NESTED_XI` | 4.8.0 |
61
- | Valved proposals, never direct mutation | `ProposeResult` then `apply` (`ARKORDER_UNVALVED_RELEASE`) | 4.8.0 / 4.8.6 |
62
- | Typed cell schema | `XiSchema` / `XiPropertySchema` | 4.8.0 |
61
+ | Valved proposals, never direct mutation | `ProposeResult` then `apply` (`ARKORDER_UNVALVED_RELEASE`); a proposal carries `baseXiHash` / `baseVersion`; a stale one, one without that binding, or one whose blast radius is not the transition that would commit fails `ARKORDER_STALE_PROPOSAL`. The binding is data, not a capability: an object that states the current base and the exact transition applies whoever built it | 4.8.0 / 4.8.6 |
62
+ | Typed cell schema | `XiSchema` / `XiPropertySchema` / `XiPrimitive` (finite numbers only; NaN / ±Infinity fail `ARKORDER_NESTED_XI`) | 4.8.0 |
63
63
  | Named slow keys on the write path | `arkOrder.xiKeys`; `ARKORDER_XI_FIELD_WRITE` | 4.8.3 |
64
64
  | Factory isolation | `createOrderPlane` from `arkgate/order` only | 4.8.0 |
65
65
  | Information budget | `informationBudget.cannotObserve`; `ARKORDER_INFORMATION_BUDGET` | 4.8.5 |
@@ -69,10 +69,10 @@ They already exist:
69
69
  | Decision tape | ArkRun information package `decisionTape` `{ xiHash, event, residual }` | 4.8.6 |
70
70
  | σ vs ξ identity | `xiHash` / `sigmaHash` / `refreshSigma` | 4.8.6 |
71
71
  | Capacity as data | `ConstraintPack.capacity` (`kind` / `sigmaKey` / `payloadKey` / `op`) | 4.8.6 |
72
- | Store port | `ReleaseStore` / `createMemoryReleaseStore` in-memory default — not durable, not K01 | 4.8.6 |
72
+ | Store port | `ReleaseStore` / `createMemoryReleaseStore` in-memory default — not durable, not K01. `load()` output is validated like `restore()`: JSON copies are accepted; hashes are recomputed from ξ / σ and an inconsistent one fails closed; a `releasedAt` (or σ.`releasedAt`) after the plane clock fails closed, so a rewritten timestamp cannot switch off σ staleness. Hashes are a consistency check, not a signature — authenticity stays the store's job. The plane advances only after `save()` returns | 4.8.6 |
73
73
  | Thin travel helper | `ingestTravelAction` absorb→`send` / escalate_up human→`raises` | 4.8.6 |
74
74
  | Verify a stored `Release.hash` | `hashOf(ξ, σ)` alias of `hashReleasePayload` — no `release()` side effect | 4.8.9 |
75
- | Reinstall a frozen Release | `restore(release)` — process-local; hash is identity; not durable; does not close K01 | 4.8.9 |
75
+ | Reinstall a Release | `restore(release)` — frozen or JSON-deserialized; hash is identity; onto an empty plane, or the same ξ at the current version or the next one. Never changes ξ, never rolls the version back, never jumps it (`ARKORDER_UNVALVED_RELEASE`); a freeze time after the plane clock fails closed. Process-local; not durable; does not close K01 | 4.8.9 |
76
76
  | Default clock | omitted `clocks` is Kernel `Date.now()`; Domain must not call `Date.now` | 4.8.9 |
77
77
 
78
78
  Nothing here is a hosted runtime. Nothing here can be “down”. A degraded-mode
@@ -102,10 +102,10 @@ const plane = createOrderPlane({
102
102
  plane.release(xi, sigma); // first freeze only
103
103
  plane.project(); // derive allowed s + invalidations
104
104
  plane.ingest(event); // residual absorb | escalate_up | hold. Never a Release
105
- plane.proposeRelease(delta); // blast radius. Empty blast = domain error
106
- plane.apply(proposal); // valve: later ξ change
105
+ plane.proposeRelease(delta); // blast radius, bound to the current Release. Empty blast = domain error
106
+ plane.apply(proposal); // valve: later ξ change; stale proposal = ARKORDER_STALE_PROPOSAL
107
107
  plane.refreshSigma(sigma); // saldo / clocks; xiHash unchanged
108
- plane.restore(release); // process-local install; hash is identity; not durable; not K01
108
+ plane.restore(release); // reinstall (same ξ, current or next version); not durable; not K01
109
109
  hashOf(xi, sigma); // same bytes as hashReleasePayload; no freeze side effect
110
110
  ```
111
111
 
@@ -114,6 +114,12 @@ There is no `update()`. Calling `update` / `patch` / `set` on the plane throws
114
114
  `ARKORDER_GENERIC_UPDATE`. A second `release()` whose ξ differs fails
115
115
  `ARKORDER_UNVALVED_RELEASE`.
116
116
 
117
+ `release`, `proposeRelease`, `apply`, `refreshSigma`, and `restore` check the
118
+ candidate projection against `informationBudget` before the Release is offered
119
+ or persisted (`ARKORDER_INFORMATION_BUDGET`), so a denied pattern never lands.
120
+ σ goes stale after a numeric `σ.freshUntil` on its own, or after
121
+ `sigmaMaxAgeMs` when σ has no `freshUntil`.
122
+
117
123
  | Field write | Verb |
118
124
  |-------------|------|
119
125
  | Invoice, seat within cap, timesheet, daily log | `ingest` → absorb (`ingestTravelAction` may `send`) |
@@ -209,13 +215,22 @@ except withdrawn heuristics.
209
215
  |------------|------|
210
216
  | `ARKORDER_MISSING_PLANE` | Extra on, no `createOrderPlane` in `planeRoots` |
211
217
  | `ARKORDER_KERNEL_IN_DOMAIN` | Domain-role layer imports `arkgate/order` |
212
- | `ARKORDER_GENERIC_UPDATE` | `update` / `patch` / `set` on the plane |
218
+ | `ARKORDER_GENERIC_UPDATE` | `update` / `patch` / `set` on a plane receiver: bound to `createOrderPlane(...)` in the file, annotated `: OrderPlane`, a `plane` / `*Plane` import from a `planeRoots` module, or a `plane` / `*Plane` name in a file that imports `arkgate/order`. A name alone (`clipPlane`, `controlPlane`) is not evidence |
213
219
  | `ARKORDER_TOO_MANY_PARAMS` | ξ keys > `maxXiKeys` |
214
- | `ARKORDER_INGEST_WRITES_XI` | `ingest` result assigned into a Release / ξ store |
215
- | `ARKORDER_XI_FIELD_WRITE` | Managed-layer driver import **and** write token **and** a declared `xiKeys` name |
216
- | `ARKORDER_INFORMATION_BUDGET` | `h(ξ)` allows a kind in `informationBudget.cannotObserve` |
217
- | `ARKORDER_XI_TTL` | ξ named ttl/freshUntil/maxAge — freshness is σ |
218
- | `ARKORDER_STALE_SIGMA` | ingest after σ.freshUntil, or after `release.releasedAt` + `sigmaMaxAgeMs` |
220
+ | `ARKORDER_INGEST_WRITES_XI` | `ingest` result assigned into a Release / ξ holder (`release`, `xi`, `xiNext`, `nextXi`, `currentRelease`, `store.xi`, …); a prefix alone (`releaseState`, `currentResidual`) is not evidence |
221
+ | `ARKORDER_XI_FIELD_WRITE` | Managed-layer driver import **and** a write on a persistence client (`db` / `prisma` / …, `new PrismaClient()`, a name bound to a driver constructor or annotated with a client type, or imported from a local `db` / `orm` / … module) **and** a declared `xiKeys` name |
222
+ | `ARKORDER_INFORMATION_BUDGET` | A literal `allowedKinds` entry that a literal `informationBudget.cannotObserve` denies |
223
+ | `ARKORDER_XI_TTL` | ttl/freshUntil/maxAge (shorthand included) in the ξ literal of `plane.release` / `plane.proposeRelease` on a plane receiver — freshness is σ |
224
+
225
+ Runtime `ArkOrderError` codes (thrown by the plane; not static sensors, not
226
+ promoted by `arkOrder.mode`):
227
+
228
+ | Code | Thrown when |
229
+ |------|-------------|
230
+ | `ARKORDER_STALE_SIGMA` | ingest after σ.freshUntil, or after `release.releasedAt` + `sigmaMaxAgeMs` (ingest returns `hold`, reason `stale-sigma`) |
231
+ | `ARKORDER_STALE_PROPOSAL` | `apply` gets a proposal not computed against the current Release |
232
+ | `ARKORDER_UNVALVED_RELEASE` | a second `release` with a different ξ, or a `restore` that changes ξ / rolls the version back |
233
+ | `ARKORDER_INFORMATION_BUDGET` / `ARKORDER_XI_TTL` | also thrown at runtime, whatever the static sensor saw |
219
234
 
220
235
  Why / fix: [diagnostics.md](diagnostics.md#ARKORDER_MISSING_PLANE).
221
236
  `ark-check --sensors` lists every extra sensor with plane, tier, and whether
@@ -230,8 +245,10 @@ package, inspector, in-memory compare). ArkOrder does not grow a bus, outbox,
230
245
  or hosted replay. [ADR 0033](adr/0033-arkorder-runtime-half-is-arkrun.md).
231
246
 
232
247
  Durability (`K01`) stays parked. In-memory is the honesty line.
233
- `restore(release)` reinstalls a frozen Release in this process. It is not a
234
- store. It does not close `K01`.
248
+ `restore(release)` reinstalls a validated Release in this process: onto an
249
+ empty plane, or the same ξ at the current version or the next one. It is
250
+ not a store and not a rollback (that is `proposeRelease` then `apply`). It does
251
+ not close `K01`.
235
252
 
236
253
  ---
237
254
 
@@ -101,7 +101,11 @@ ark-check --rules-inventory --json
101
101
  # MCP: ark_rules_inventory
102
102
  ```
103
103
 
104
- Output is **honest counts** (inventoried / under-contract / frozen) — never a score. Route
104
+ Output is **honest counts** (inventoried / under-contract / frozen) — never a score. A
105
+ structure candidate is under contract when its layer declares a rule on the suggested
106
+ **sensor** (the rule id is yours to name); an invariant candidate by its id. Frozen counts the
107
+ ArkRules-plane keys (`ARKRULE_*` / `INVARIANT_*`) in `.ark-baseline.json`. `--doctor` shows the
108
+ same counts as `rulesMigration` (JSON and human line), and the MCP payload matches the CLI. Route
105
109
  extraction through `/ark-fix` or `/ark-loop` (one pilot card at a time) and declaration
106
110
  through `/ark-contract` editing `arkrules/<Layer>.json` (ADR 0015 — no new skill names).
107
111
  Report residual as **`[Layer]`** (import edges / baseline) vs **`[ArkRules]`** (inventory /
@@ -74,7 +74,9 @@ Invalid ArkGate config (/repo/ark.config.json):
74
74
  - $.layers[0].forbiddenGlobal: unknown field
75
75
  ```
76
76
 
77
- The same input cannot pass CI while being silently ignored by MCP or ESLint. Invalid JSON, wrong
77
+ The same input cannot pass CI while being silently ignored by MCP or ESLint. In ESLint an
78
+ invalid contract (root config or a referenced ArkRules file) is one `configInvalid` error per
79
+ linted file with the same validator text; it does not crash the lint run. Invalid JSON, wrong
78
80
  types, empty required strings, duplicate string-array entries, negative safety thresholds, and
79
81
  unsupported schema versions also fail before scanning begins.
80
82
 
@@ -130,9 +132,24 @@ Top-level fields:
130
132
  `enforced`. Declaring nothing stays silent unless any invariant is already enforced — then
131
133
  missing roots fail closed (`INVARIANT_COVERAGE_ROOTS_MISSING`), because otherwise coverage can
132
134
  certify a test the project never declared a runner root for.
133
- - **`arkRules`** (optional, schema `1.1+`) — map of layer name → project-relative path to an
134
- ArkRules file (e.g. `"DomainModel": "arkrules/DomainModel.json"`). Keys must match a declared
135
- layer. Missing/invalid referenced files **fail closed**.
135
+
136
+ ArkGate never executes tests — except the opt-in `arkgate-check --probe-invariants`, which
137
+ you run yourself: it breaks an invariant's `coverage.symbol` on purpose in a temporary copy
138
+ and runs only its covering tests with your own runner. Its committed result
139
+ (`.ark/invariant-probe.json`) can only **refuse** a promotion, never grant one, and it is
140
+ never part of a check or the merge gate. No config key. See
141
+ [Probe an invariant](agent-guide.md#probe-an-invariant).
142
+ - **`arkRules`** (optional, schema `1.1+`) — map of layer name → one project-relative path, or a
143
+ list of paths merged into that layer's catalog (`"DomainModel": "arkrules/DomainModel.json"`
144
+ or `"DomainModel": ["arkrules/DomainModel.json", "arkrules/DomainModel.shared.json"]`). A
145
+ string keeps today's behaviour. Keys must match a declared layer. The same effective id in
146
+ two files is `ARKRULE_DUPLICATE_ID` and config load fails closed. Missing or invalid
147
+ referenced files **fail closed**. A `arkrules/*.json` file the map does not reference, and a
148
+ slice file named `arkrules.<Layer>.json` beside governed code that discovery did not read,
149
+ is advisory drift: `ARKRULE_FILE_UNREFERENCED` in check `warnings`,
150
+ `rulesUnderContract.unreferencedFiles` in doctor, and `arkRulesCatalog.unreferencedFiles` in
151
+ MCP `ark_manifest`. It never fails the check. Doctor `rulesUnderContract.bySlice` lists the
152
+ rules a child slice contributed. `ark_place` shows those ids as `sliceRules` next to the layer.
136
153
  - **`arkRun`** (optional, schema `1.2+`) — inline ArkRun extra (`mode`, `kernelRoots`
137
154
  (`compositionRoots` alias), `managedLayers`, `requireDeclarations`). Absence is silent. Unknown keys fail closed.
138
155
  `managedLayers` must name existing `layers[].name` values. Empty `compositionRoots` in
@@ -142,6 +159,19 @@ Top-level fields:
142
159
  is a policy-delta **weakening**. Enforced extra teeth share the CLI / MCP / hook /
143
160
  preflight / CI verdict and arm only when the layer plane is classified (same ≥50%
144
161
  governed and ≥1 populated-layer floor as ArkRules).
162
+ Kernel factories in a root: `createArkKernel` / `createStrictArkKernel` /
163
+ `createLenientArkKernel`, their `*FromConfig` variants, or `ArkModule.forRoot()` /
164
+ `forRootAsync()` imported from `arkgate/nestjs`. Undeclared-* sensors count only calls
165
+ whose receiver is traced to the kernel (factory-bound local, import from a root module —
166
+ resolved through tsconfig `paths` and barrels that re-export it — a plain alias
167
+ `const kernel = ark`, a `typeof ark` binding, a binding typed `ArkKernel` / `EventBus` /
168
+ publisher from `arkgate/runtime` or `arkgate/nestjs`, its `.eventBus`, a `publisher(..)`
169
+ result), or whose receiver cannot be traced but whose literal name is a kernel-valid
170
+ intent (`Domain.…`, `Application.…`) — `res.send('ok')` or `require.resolve('pkg')` never
171
+ count. Call names are a string literal or a same-file
172
+ `define(..)` / `defineIntent(..)` creator (or string constant); anything else in
173
+ `enforced` mode reports `ARKRUN_INTERACTION_NAME_INCOMPLETE` (partial). See
174
+ [diagnostics](diagnostics.md#ARKRUN_MISSING_ROOT).
145
175
  - **`arkOrder`** (optional, schema `1.3+`) — inline ArkOrder extra (`mode`, `planeRoots`,
146
176
  `managedLayers`, `maxXiKeys`, **`xiKeys`**, optional **`appliesTo`**). Absence is silent.
147
177
  Unknown keys fail closed. Import `createOrderPlane` from `arkgate/order` (same package).
@@ -157,7 +187,11 @@ Top-level fields:
157
187
  at least one glob. Membership ids and recomputable statuses such as `paid` /
158
188
  `overdue` are not keys. Factory options `informationBudget`, `sigmaMaxAgeMs`,
159
189
  `store` (`ReleaseStore`), and capacity packs belong on `createOrderPlane`, not this
160
- extra object. Later ξ is `proposeRelease` then `apply`; `refreshSigma`; ingest
190
+ extra object. Literal evidence of either still reports statically: a ttl/freshUntil/maxAge
191
+ key in a plane `release` / `proposeRelease` ξ literal (`ARKORDER_XI_TTL`), and a literal
192
+ `allowedKinds` entry denied by a literal `cannotObserve` (`ARKORDER_INFORMATION_BUDGET`).
193
+ Later ξ is `proposeRelease` then `apply` (a stale proposal fails
194
+ `ARKORDER_STALE_PROPOSAL`); `refreshSigma`; ingest
161
195
  residual `absorb | escalate_up | hold`. Demotion or deletion is a policy-delta
162
196
  **weakening**. Field ingest never mints a pattern.
163
197
 
@@ -230,6 +264,18 @@ That sentence is product copy. Not “Rich domain model, business rules, and dom
230
264
  ```
231
265
 
232
266
  - `intentPrefixes`, `forbiddenGlobals`, `mayImportInfrastructure`, `optional`
267
+ — `intentPrefixes` is how the ArkRun kernel maps an intent name to a layer at runtime.
268
+ A layer with a canonical 11-layer name (`DomainModel`, `ApplicationOrchestration`,
269
+ `PersistenceAdapters`, … — what `ark init` writes) and no `intentPrefixes` gets the
270
+ built-in prefixes (`Domain.`, `Application.`, `Adapter.Persistence.`, …), so default configs
271
+ enforce hard observed-layer-flow as-is. A custom-named layer in an `allowed: false` rule
272
+ with no `intentPrefixes` cannot map any intent: `createStrictArkKernelFromConfig` /
273
+ `createArkKernelFromConfig` still build (strict default) and record one
274
+ `layer.observedFlowUnresolvable` audit record (`ARKRUN_LAYER_FLOW_UNRESOLVABLE`), and
275
+ `ark doctor` lists those layers when the `arkRun` extra is on. Passing
276
+ `enforceObservedLayerFlow: 'hard'` explicitly throws `ArkKernelConfigError` instead.
277
+ `peerIsolation` rules are file-path slice walls; runtime flow (producer/intent names only)
278
+ does not evaluate them.
233
279
  - `reserved` / `allowEmpty` — future houses whose globs match nothing yet. `--strict-config` does not fail; `CONFIG_LAYER_PATTERN_NO_MATCHES` (typo warning) is skipped. A typo warning fires only when the glob is not reserved.
234
280
  - `capabilities: { deny: [...] }` — opt-in effect walls over the seven capability ids
235
281
  (`network`, `filesystem`, `clock`, `randomness`, `environment`, `process`, `persistence`);
@@ -256,6 +302,8 @@ Rule fields:
256
302
  when both paths classify. Applies to **any** declared `from`→`to` pair, not only self-edges.
257
303
  Missing paths, empty slice folders, or unclassifiable slices **fail closed** (deny — cannot
258
304
  prove same-slice).
305
+ The wall denies imports; status details (`--doctor --all`) list code copied across it
306
+ (`doctor.crossWallDuplication`, advisory, no config key).
259
307
  - `sliceFolders` is a bare name or a starred prefix. A bare name (`features`) stays an
260
308
  unanchored one-segment match: `src/lib/features/projects/rfi/x.ts` with `["features"]` is
261
309
  `features/projects`. The next segment is never a filename, so
@@ -269,19 +317,43 @@ Rule fields:
269
317
  off that prefix (`src/app/api/projects/...`) is not that slice.
270
318
  - `sliceIdentity` is `path` or `stars`. Absent and `path` are the same: today's ids,
271
319
  byte for byte, so baselines and `allowedCrossSlice` keep working with no migration.
272
- `stars` names a starred prefix as the last literal plus the star bindings.
320
+ `stars` names a starred prefix as the last literal plus every star binding. It
321
+ drops only the literals before the last literal; a star before it is kept, so
322
+ `modules/*/api/*` binds `orders/api/v1` and `users/api/v1`, never one shared `api/v1`.
323
+ arkgate 4.8.23 dropped that star too (`api/v1`). For one release a `sliceAliases`
324
+ `to`, an `allowedCrossSlice` entry, or a `childSlices.allowedCrossSlice` entry written
325
+ against the 4.8.23 id still loads and keeps its 4.8.23 verdict: a legacy alias target
326
+ (`api/v1/x`) joins the universe whose 4.8.23 id it names on each edge, and a legacy
327
+ allowance still clears the edges it cleared. ark-check warns `CONFIG_SLICE_LEGACY_STARS_ID`
328
+ (`failsStrict: false`) and names the new id to write (`orders/api/v1/x`, or one entry
329
+ per module when several modules share that old id). A legacy alias target is rejected at
330
+ config load only when it maps to more than one universe shape of the rule
331
+ (`maps to more than one universe shape`). Rewrite these entries before the next minor
332
+ release, which drops the legacy form.
273
333
  `lib/features/*/*` and `lib/repositories/features/*/*` both yield
274
334
  `features/projects/rfi`, so one feature has one id across parallel trees. Bare names
275
335
  (`features`) are unchanged. Doctor warns when two different prefixes bind as the
276
336
  same id, and the warning names both paths (`admin/features/*` and `public/features/*`
277
337
  both bind as `features/*`). That warning is advisory. Unrelated trees that share a
278
338
  last folder name should stay on `path`.
279
- - `childSlices` is an optional inner wall on a `peerIsolation` rule. Absent means today's
339
+ - `childSlices` is an optional inner wall on a `peerIsolation` rule. It runs only with
340
+ `peerIsolation: true` and `allowed: false` on the same rule. Elsewhere it is inert: config
341
+ load still accepts it (4.8.23 did), and ark-check warns `CONFIG_CHILD_SLICES_INERT`
342
+ (`failsStrict: false`) with the fix — add both flags to the rule, or remove `childSlices`.
343
+ Absent means today's
280
344
  universe wall, byte for byte: no `reasonId`, same messages, same doctor output.
281
- `sliceFolders` names the children (`lib/features/*/*` under a universe id `features/projects`).
282
- `sliceIdentity` is the same `path` | `stars` choice. `commonFolders` (directory names such as
283
- `domain`) and a flat file whose child id does not grow past the universe id are universe
284
- common. A child may import that common code. Common code may import a child only when
345
+ `sliceFolders` names the children. `sliceIdentity` is the same `path` | `stars` choice, and
346
+ the child id must extend the universe id. With `"sliceIdentity": "stars"`, `lib/features/*/*`
347
+ yields child ids like `features/projects/rfi` under the universe id `features/projects`. Under
348
+ the default `path` identity the child id keeps the full prefix (`lib/features/projects/rfi`),
349
+ which does not extend a bare-name universe id such as `features/projects`: the check warns
350
+ `CONFIG_CHILD_SLICE_EXTENDS` and the child wall stays off. Pair a bare-name universe with
351
+ `"sliceIdentity": "stars"` on `childSlices`. A flat file whose child id does not grow
352
+ past the universe id is universe common. So is a `commonFolders` directory (a name such as
353
+ `domain`) **directly under the universe**: `lib/features/projects/domain/**`. A folder of
354
+ that name inside a child (`lib/features/projects/rfi/domain/**`) belongs to that child, so a
355
+ sibling that imports it is `CROSS_SIBLING_SLICE` and the child's own `domain/` may import
356
+ its own feature. A child may import universe common code. Common code may import a child only when
285
357
  `parentMayImportChild` is true (default false). `siblings` is `deny` (default, also when the
286
358
  key is omitted), `advisory`, or `{ "default": "deny" | "advisory", "enforce": [...] }`.
287
359
  The universe wall runs first. A denied cross-universe edge is
@@ -293,15 +365,43 @@ Rule fields:
293
365
  (`lib/features/projects/rfi` or `src/lib/features/projects/rfi`). A child id matches that
294
366
  importer's child id. A path matches when the importer file sits in that directory.
295
367
  Bare names do not match. `*` is not a wildcard on this list. `default: "deny"` denies every
296
- sibling crossing; the list cannot loosen it. A new advisory crossing past the recorded
297
- baseline still fails, the same ratchet as a string `advisory`. An enforce list does not
298
- change that function. The baseline key stays `ruleId|file|fromLayer|toLayer|target` and does
368
+ sibling crossing; the list cannot loosen it. Advisory is advisory on day 1: with
369
+ `--baseline` (or an `--against` base), the anti-growth ratchet is judged **per rule** and
370
+ only over advisory crossings. `siblings.ratchet` on the object form picks it:
371
+ absent — on only when the baseline in use already records at least one advisory
372
+ crossing of that rule, and then only a crossing past that recorded count fails (an empty
373
+ baseline, or one frozen before the child wall, promotes nothing). The recorded count is
374
+ what the baseline holds for that rule: a key whose crossing was removed still counts when
375
+ the rule still classifies that edge as an advisory sibling crossing, so swapping one
376
+ recorded crossing for a new one keeps the count and stays a warning; `true` — any unrecorded
377
+ advisory crossing fails whenever a baseline is in use (the durable lock); `false` — measure
378
+ only, never fails (doctor `slices.crossSibling` still counts). A recorded enforced crossing
379
+ never switches the advisory ratchet on. A promoted finding says why and names
380
+ `ratchet: false`. `enforce` importers and `default: "deny"` stay errors either way. Doctor
381
+ `productHonesty` applies the same ratchet as `ark-check --baseline`. `ratchet: false` is a
382
+ weakening policy delta and `ratchet: true` a strengthening one. arkgate 4.8.23 and older
383
+ reject `ratchet` (`unknown field` at `$.rules[n].childSlices.siblings.ratchet`).
384
+ `childSlices.message` is the text for inner-wall findings (`CROSS_SIBLING_SLICE` and
385
+ universe common importing a child). The rule `message` stays the universe-wall text
386
+ (`CROSS_PARENT_SLICE` and the fail-closed denies). Without `childSlices.message`, an
387
+ inner-wall finding gets ArkGate's own default text; it never reuses the rule message.
388
+ Every surface uses the same choice: ark-check / CI, the write hook, MCP `validate_code`,
389
+ ESLint, and `analyzeProject` / prepare-change. Editing it is not a policy delta. arkgate
390
+ 4.8.23 and older reject it (`unknown field` at `$.rules[n].childSlices.message`). The baseline key stays `ruleId|file|fromLayer|toLayer|target` and does
299
391
  not include `reasonId`. `productHonesty.finished` stays false while any child wall is
300
392
  advisory, including `{ "default": "advisory", "enforce": [...] }`. The list does not finish
301
393
  the house. Doctor `slices` is `{ crossParent, crossSibling, pairs }` for the directed universe
302
394
  pairs that occur. A child id that does not extend its universe id warns
303
395
  `CONFIG_CHILD_SLICE_EXTENDS` and is treated as universe common. arkgate 4.8.22 and older
304
- reject `childSlices` (`CONFIG_CHILD_SLICES_VERSION`). A build that still types `siblings` as
396
+ reject `childSlices`. Ark warns `CONFIG_CHILD_SLICES_VERSION` only when the repo pins such
397
+ an older arkgate: an exact `package.json` dependency, the installed `node_modules/arkgate`,
398
+ `package-lock.json`, a `scripts/*hook*` / `.husky` / host hook command, or a CI workflow
399
+ (`pedroknigge/arkgate@vX.Y.Z` or its `version:` input). The finding points at that file and
400
+ line and says which version to bump to. Without such evidence it is silent. Ranges
401
+ (`^4.8.0`) are not evidence; pnpm/yarn lockfiles are read through the installed copy. The
402
+ same check covers `sliceIdentity` (4.8.21), `sharedImportsSlice` (4.8.20),
403
+ `"deny-cross-parent"` (4.8.23), and the keys added after 4.8.23 (`stopAt`,
404
+ `childSlices.message`, `siblings.ratchet`). A build that still types `siblings` as
305
405
  the string enum `deny | advisory` rejects the object at config load
306
406
  (`must be one of deny, advisory` at `$.rules[n].childSlices.siblings`). It does not ignore
307
407
  the object. No published release through 4.8.22 accepts `{ default, enforce }`. Ship that
@@ -326,10 +426,16 @@ Rule fields:
326
426
  Ship it only on a release that includes this field.
327
427
  `childSlices.sliceAliases` maps files that sit outside every slice folder onto
328
428
  a child that already exists: `{ "from": "lib/compliance/**", "to": "features/projects/compliance" }`.
329
- `to` is the universe id the universe wall already knows, plus one child segment.
330
- A bare name, that universe id alone, an unknown universe, and a wildcard in `to`
331
- are rejected at config load (`child of an existing universe`). The source glob
332
- may cover only files the slice folders do not already classify. Overlap with a
429
+ A `from` without a wildcard in its last segment is a folder form and also covers
430
+ everything under it, as `sharedRoots` does (`lib/compliance` equals `lib/compliance/**`).
431
+ `to` must match a universe `sliceFolders` shape of the rule, plus one child segment.
432
+ A bare name, a universe id alone, a target outside every universe shape, and a
433
+ wildcard in `to` are rejected at config load (`child of a universe shape`); under
434
+ `"sliceIdentity": "stars"` a target written against the 4.8.23 id loads with a
435
+ `CONFIG_SLICE_LEGACY_STARS_ID` warning (see `sliceIdentity` above). Config
436
+ load cannot see the tree, so it checks only the shape: doctor marks an alias move
437
+ whose target universe no scanned file belongs to (`unknownUniverse`, "check for a
438
+ typo"). The source glob may cover only files the slice folders do not already classify. Overlap with a
333
439
  universe `sliceFolders` entry or a child `sliceFolders` entry is rejected
334
440
  (`overlaps a slice folder`). Two aliases that can match one file are rejected
335
441
  (`two slice aliases match the same file`). An aliased file takes the target
@@ -337,9 +443,23 @@ Rule fields:
337
443
  universe is `CROSS_PARENT_SLICE` and an import into another child of the same
338
444
  universe is `CROSS_SIBLING_SLICE`. Same-child imports and imports into that
339
445
  universe's common folders follow the existing child wall. Doctor lists every
340
- alias as an owed move: the source glob, the target slice, the destination
341
- folder, and the files. The section says the files are not finished. Aliases
342
- are debt. `productHonesty.finished` stays false while any alias is set.
446
+ unpinned alias as an owed move: the source glob, the target slice, the destination
447
+ folder, and the files. The section says those files are not finished.
448
+ `"pinned": true` lists the alias under pinned instead. `reason` (for example
449
+ `"framework-route"`) is the label doctor prints. A reason alone does not pin.
450
+ A pinned route is not `slice-alias-debt` and does not clear any other honesty
451
+ reason, so `productHonesty.finished` stays false while real debt remains.
452
+ `childSlices.arkRulesFile` is a filename at each child root the wall already
453
+ resolved (`"arkrules.<Layer>.json"`). `<Layer>` is the only token. The file is
454
+ law: it enters `policyHash`, and a path added or removed is one
455
+ `arkrules-ref-added` or `arkrules-ref-removed`. The effective id is
456
+ `<childId>#<localId>`. Omitting `appliesTo` scopes the rule to the slice
457
+ directory. An explicit `appliesTo` outside that directory is
458
+ `ARKRULE_SCOPE_ESCAPES_SLICE` and config load fails closed. A filename
459
+ `arkrules.<Layer>.json` is a law path for mixed-law classification. An older
460
+ build rejects the array form, `arkRulesFile`, `pinned`, and `reason` at config
461
+ load. Aliases
462
+ are debt while any unpinned alias remains. `productHonesty.finished` stays false while that debt, or any other honesty reason, remains.
343
463
  Adding aliases is a strengthening policy delta (unclassified files come under
344
464
  the walls). Removing them is weakening (those files leave the walls) and needs
345
465
  the same hash-bound acknowledgement as any other weakening. Additions and
@@ -353,13 +473,20 @@ Rule fields:
353
473
  It does not ignore the key. No published release through 4.8.22 accepts
354
474
  `sliceAliases`. Ship a config that sets it only on a release that includes
355
475
  this field. The write hook, ESLint, snippet analysis, ark-check, and CI all
356
- see the per-edge decision.
476
+ see the per-edge decision. In ESLint an error-level crossing reports on
477
+ `ark/no-domain-infra-imports` and an advisory (warning) crossing reports on the
478
+ warn-level `ark/architecture-advisory`, because ESLint severity is per rule id
479
+ ([ai-gates — ESLint](ai-gates.md#eslint-editor-feedback--bounded-parity-envelope)).
357
480
 
358
481
  Doctor may suggest moving a flat file that sits at universe level when
359
482
  exactly one child slice imports it. The card names the file, that child, and
360
483
  a destination folder inside the child that keeps the file's layer. It is a
361
484
  suggestion. It is not a config field, not a finding, and not an ark-check
362
- result. A file two children import stays where it is.
485
+ result. The importers come from the same resolved graph ark-check uses (tsconfig
486
+ path aliases included). A file two children import stays where it is, and so
487
+ does a file that anything outside that one child imports (universe common, a
488
+ shared root, another universe, another layer). A move that the walls would deny
489
+ for any recorded importer is never suggested.
363
490
 
364
491
  ```jsonc
365
492
  {
@@ -437,6 +564,30 @@ is evidence:
437
564
  That pass runs in `ark-check` and CI. The write hook and ESLint see one edge at a time
438
565
  and do not block it. arkgate 4.8.22 and older reject the value at config load (`must be
439
566
  one of deny`). They do not ignore it and they do not treat it as `"deny"`.
567
+ A universe edge the importer's rule declares in `allowedCrossSlice` (directed) is not
568
+ reported through a shared root either: it is the same dependency the direct wall
569
+ already allows. `childSlices.allowedCrossSlice` still never clears it.
570
+ In a DI app the composition root (bootstrap, registrations) has to sit under
571
+ `sharedRoots`, and every slice that asks the root for a service then "reaches" every
572
+ universe the root registers. Name those files in the object form:
573
+ `"sharedImportsSlice": { "mode": "deny-cross-parent", "stopAt": ["kernel/bootstrap.ts",
574
+ "kernel/registrations/**", "lib/feature-registrations.ts"] }`. The walk never starts at
575
+ or passes through a stop file, so a path through it is not a crossing. A `stopAt` entry
576
+ matches like a shared root (anchored, optional leading `src/` or `app/`, a plain folder
577
+ covers its subtree) or like an alias glob (`kernel/registrations/**` against
578
+ `src/kernel/registrations/x.ts`). Config load rejects an entry that covers the whole
579
+ tree (`*`, `**`, `src`, `src/**`, `app/**`, `*/**`) or a whole layer root of that rule
580
+ (layer `src/lib/**` with `lib/**`), since it would stop every shared node and silence
581
+ the walk: `must not cover the whole tree` / `must not cover a whole layer root of this
582
+ rule`. A stop file must still
583
+ sit under `sharedRoots`; `stopAt` does not classify files and does not silence the direct
584
+ `SHARED_IMPORTS_SLICE` warning. A slice destination that matches a stop is still reported.
585
+ Each finding carries `via` (the shared hops). Doctor `sharedWalkHubs` names a shared file
586
+ that sits on at least half of ten or more such findings — a likely composition root. That
587
+ is advisory, not a finding. Adding stops is a weakening policy delta
588
+ (`shared-walk-stop-added`), removing them strengthens, both together need a judgment. The
589
+ string forms are unchanged. arkgate 4.8.23 and older reject the object at config load
590
+ (`must be one of deny, deny-cross-parent`).
440
591
  - `allowedCrossSlice` entries match a full slice id (`features/catalog`) or a bare slice name
441
592
  (`catalog`), and only in the direction written. The reverse edge still denies. A bare name
442
593
  matches that name under **any** slice folder, so in a repo with several slice parents
@@ -448,7 +599,10 @@ is evidence:
448
599
  about your code) versus `unclassifiable path (src/widgets/x.tsx)` (a fact about our evidence).
449
600
  `no slice folders` and `no path evidence` are the two remaining evidence reasons. A rule-level
450
601
  `message` override no longer hides it: the reason is appended to your text, not replaced by it.
451
- - Both declarations are **weakening** changes in `ark policy-delta`
602
+ The write hook and MCP `validate_code` say the same, and for an evidence reason their fix
603
+ asks you to move the file into a slice or declare its root in `sharedRoots`.
604
+ - Both declarations are **weakening** changes in the policy delta (`arkgate-check --strict-merge`
605
+ with `--policy-base <file>` / `--policy-base-ref <ref>`, or MCP `ark_policy_delta`)
452
606
  (`shared-roots-added`, `cross-slice-allowance-added`), so a policy review sees them. Both are
453
607
  inert on a rule without `peerIsolation: true`, and policy-delta stays silent about them until
454
608
  the wall exists.
@@ -464,7 +618,8 @@ other choice deliberately, not so slices can drift into a mesh.
464
618
  runtime coupling. They still appear on the **violations** list with `typeOnly: true`,
465
619
  `failsStrict: false`, and adapter diagnostic **severity: warning** so doctor/HTML keep
466
620
  `violations.typeOnly` / `typeEdgePolicy` honest — but they **do not** fail merge/exit, library
467
- `valid`, or preflight the way **value** edges do. **Exception:** `peerIsolation` slice
621
+ `valid`, or preflight the way **value** edges do. ESLint reports them as warnings on
622
+ `ark/architecture-advisory`, not as errors. **Exception:** `peerIsolation` slice
468
623
  boundaries stay hard even for type-only. A value import of a pure-type barrel is still a value
469
624
  edge (not soft-skipped). Prefer placing shared types in a **SharedTypes** (or owning) layer
470
625
  both sides may import. Optional starter: [`templates/layers/shared-types.starter.json`](../templates/layers/shared-types.starter.json)
@@ -526,8 +681,8 @@ Each `arkrules/<Layer>.json` may declare:
526
681
 
527
682
  | Section | Purpose | Modes | What it really enforces |
528
683
  |---------|---------|--------|-------------------------|
529
- | `structure[]` | Closed sensor ids (e.g. `orchestration-only`, `thin-adapter`, `writes-via-aggregate`, `aggregate-private-state`, `always-valid-factory`, `domain-event-on-mutation`, `no-anemic-model`) | `advisory` (default) or `enforced` | **Heuristics of module shape** — not proof that logic was extracted to Domain. `writes-via-aggregate` is driver-import + write-token in the declaring layer (ADR 0032). Tier-2 sensors (`no-anemic-model`) stay advisory-only (cannot promote to enforced). |
530
- | `invariants[]` | Stable ids + description + `coverage` (`test` / `symbol`) + optional `appliesTo` globs | `advisory` or `enforced` | **Named policy + evidence.** `coverage.symbol` means a declaration of that identifier in a non-test file (witness path `symbolEvidenceFile`; additive `shape`: `function`, `class`, `const`, `method`, `type`, `interface`, or `enum`). Imports and test calls do not count. A `describe` / `it` / `test` / `context` title may also name the invariant id. A comment, string, or other bare mention does not. Does **not** execute tests or business logic and does **not** replace behavior/property tests. Doctor and `--rules-inventory` print the witness path and discard counts on green runs too. A declaration match is not “the tests pass.” |
684
+ | `structure[]` | Closed sensor ids (e.g. `orchestration-only`, `thin-adapter`, `writes-via-aggregate`, `aggregate-private-state`, `always-valid-factory`, `domain-event-on-mutation`, `no-anemic-model`) | `advisory` (default) or `enforced` | **Heuristics of module shape** — not proof that logic was extracted to Domain. `writes-via-aggregate` is a driver import plus a write token, or a SQL write head inside a tagged template, in the declaring layer (ADR 0032). Tier-2 sensors (`no-anemic-model`) stay advisory-only (cannot promote to enforced). Class shapes come from a tokenizer that skips strings, comments, template literals and regex literals; a class it cannot walk to its end is reported as `shape analysed until character N` (an error under an enforced sensor), never passed in silence. |
685
+ | `invariants[]` | Stable ids + description + `coverage` (`test` / `symbol`) + optional `appliesTo` globs | `advisory` or `enforced` | **Named policy + evidence.** `coverage.symbol` means a declaration of that identifier in a non-test file (witness path `symbolEvidenceFile`, project-relative; additive `shape`: `function`, `class`, `const`, `method`, `type`, `interface`, or `enum`). A `Class.member` symbol (e.g. `Order.ensureInvariants`) requires a member with that name inside the body of `class Class`, a `const Class = class { … }` expression, a `namespace Class { … }` (function or const), or a top-level key of a `const Class = { … }` object literal; a member of another container, a nested object key, a free function, or a file that only mentions the class name does not count. The search is not restricted to the invariant's layer. `coverage.test: false` without a `coverage.symbol` declares no evidence: it is advisory-only (an enforced invariant with that shape reports `INVARIANT_UNCOVERED` and fails strict; the rest of the catalog still evaluates) and cannot be promoted. Imports and test calls do not count. A `describe` / `it` / `test` / `context` title may also name the invariant id. A comment, string, or other bare mention does not. Does **not** execute tests or business logic and does **not** replace behavior/property tests. Doctor and `--rules-inventory` print the witness path and discard counts on green runs too. A declaration match is not “the tests pass.” |
531
686
 
532
687
  **Reporting:** diagnostics carry `evidence.arkruleId` + `evidence.arkruleSource`. Label residual
533
688
  **`[Layer]`** vs **`[ArkRules]`** in agent output. Doctor / HTML: `rulesUnderContract` (catalog +
@@ -571,6 +726,14 @@ npx ark-check --strict-config --policy-base ./before.ark.config.json --json
571
726
  npx ark-check --strict-merge --policy-base-ref origin/main
572
727
  ```
573
728
 
729
+ ArkRules are part of the policy. With a Git ref, the base `arkrules/*.json` files are read from
730
+ that same ref (a file the base config references but the ref does not contain yet counts as an
731
+ empty layer). With `--policy-base <file>`, the base ArkRules paths resolve next to that file (keep
732
+ a copy of the base `arkrules/` beside it; the error names that directory and the
733
+ `--policy-base-ref` alternative). A base ArkRules file that cannot be read or parsed fails
734
+ closed, because comparing the candidate catalog with itself would read every demotion or
735
+ deletion as neutral.
736
+
574
737
  The additive JSON result includes `policyDelta`: both policy hashes, the overall classification,
575
738
  stable findings, and `blockingFindingIds`. Supported comparisons cover governed include/exclude
576
739
  roots, layer patterns/exclusions/forbidden globals, deny rules, same-layer peer isolation,
@@ -627,14 +790,40 @@ type than product source:
627
790
 
628
791
  | Check | What it does |
629
792
  |-------|----------------|
630
- | `ark-check --local --base origin/dev` | Opt-in local / multi-worktree cheap check. Same engine as `--changed`. Refused with `--strict-merge`. `ARK_CHECK_LOCAL=1` is the same unless CI or a full-tree report mode is on. |
793
+ | `ark-check --local --base origin/dev` | Opt-in local / multi-worktree cheap check. Same engine as `--changed`. Refused with `--strict-merge` and with `--update-baseline`. `ARK_CHECK_LOCAL=1` is the same unless CI, a full-tree report mode, or `--update-baseline` is on. |
631
794
  | `ark-check --changed --base origin/dev` | Layer check on touched sources only. A CSS/i18n PR pays almost nothing. |
632
795
  | `ark-check --against origin/dev` | New violation keys vs **that ref's** baseline (not only HEAD). |
633
796
  | `ark-check --contract-diff --base origin/dev` | Classifies tighten / loosen / reclassify / baseline-grow. |
634
- | `--contract-session --author <id>` | Law-only PR. Mixed law+product still fails. Loosen/grow need a session even with an empty `stewards[]`; a non-empty list also needs a matching listed author. |
797
+ | `--contract-session --author <id>` | Law-only PR. Mixed law+product still fails. Loosen/grow need a session even with an empty `stewards[]`; a non-empty list also needs a matching listed author. A session always classifies the law change against the team base (`--base`, or the discovered `main`/`dev`), including under `--strict-merge`. If that base has no contract yet (the adoption PR that adds `ark.config.json`), there is nothing to loosen and the session proceeds; an explicit `--policy-base-ref` without a contract at that ref still exits 2. |
635
798
  | `--persona touch\|contributor\|agent\|steward` | Budget presets for the same teeth. |
636
799
  | `ark status --vs origin/dev` | One line: pin / contract / baseline drift vs that ref. |
637
800
 
801
+ How the changed-file set is computed (`--changed`, `--local`, `--against`, `--persona`):
802
+
803
+ - Paths are relative to `--root`, even when that root is a package inside a monorepo
804
+ (`git diff --relative`; base-ref files are read as `<ref>:./<path>`). Changes outside the
805
+ root are not part of that package's diff.
806
+ - Committed changes since the merge base, staged, unstaged, and untracked files all count.
807
+ File names with spaces, quotes, or non-ASCII characters are kept exactly.
808
+ - Without `--base` / `--against`, the base is the first of `origin/dev`, `origin/main`,
809
+ `origin/master`, `dev`, `main`, `master` that exists. When none exists (or the root is
810
+ not a git repository), `--changed` and a non-steward `--persona` stop with **exit 2**
811
+ (`reasonId: changed-needs-base`; `--local` uses `local-needs-base`). `--contract-diff`
812
+ and `--persona steward` (which implies it) also stop with **exit 2**
813
+ (`reasonId: contract-diff-needs-base`): the contract and baseline diff has nothing to
814
+ compare against. Pass `--base <ref>` or run a plain full check. A diff that could not be
815
+ computed never counts as "no changes".
816
+ - Any failed git listing (bad ref, timeout, output too large) is exit 2
817
+ (`reasonId: changed-paths-unavailable`), never a pass.
818
+ - `--report` applies the committed `.ark-baseline.json` implicitly, except under
819
+ `--strict` / `--strict-merge` / `--contract-diff` / `--against`: a merge verdict uses a
820
+ baseline only when `--baseline` is passed.
821
+ - `--update-baseline` always freezes the whole tree: it refuses `--changed` / `--local`
822
+ (exit 2) and ignores `ARK_CHECK_LOCAL=1`, so a partial scan can never truncate
823
+ `.ark-baseline.json`.
824
+ - Layer-pattern typo warnings (`CONFIG_LAYER_PATTERN_NO_MATCHES`) under `--changed` are
825
+ judged against the full governed tree, so an untouched layer is not reported as a typo.
826
+
638
827
  Write-gate ApplyPatch denies a batch that mixes law files with product source. Humans who
639
828
  never hit PreToolUse are unchanged. Local `pnpm` gates should call `--local --base` or
640
829
  `--changed --base`, not only full-tree `--strict-merge`. Write hooks stay on the lexical
@@ -645,11 +834,21 @@ worktree has its own root); there is no machine-wide analysis lock.
645
834
  include tree. File-local ArkRules sensors (class shape, orchestration-only, thin-adapter,
646
835
  writes-via-aggregate) and hint preload stay on the touched set. Layer and cycle sensors
647
836
  see that closure, so a new illegal import or a cycle the change can complete still
648
- fails. Untouched files outside the closure are left to the full-tree CI check. Same
837
+ fails. When a touched file sits under the `sharedRoots` of a `"deny-cross-parent"` rule,
838
+ the graph runs full-tree: the finding belongs to the slice that imports the shared root,
839
+ which may be outside the closure. A `CROSS_PARENT_VIA_SHARED` path counts as part of the
840
+ change when any file on it changed. Untouched files outside the closure are left to the full-tree CI check. Same
649
841
  engine; not a second analysis path. A `--changed` pass is not a full-tree structural
650
842
  verdict.
651
843
 
652
844
  MCP clients can call `ark_policy_delta` with the previous `baseConfig`, an optional candidate
653
845
  contract (the current project contract is the default), and the same optional acknowledgement.
846
+ When `baseConfig` maps `arkRules`, pass the base catalog as data in `baseArkRuleFiles`
847
+ (`{ "<path from baseConfig.arkRules>": <ArkRules file JSON> }`); without it the call is refused
848
+ instead of returning a config-only verdict. When `childSlices.arkRulesFile` is set, include each
849
+ discovered slice file in that same map, keyed by its project-relative path. The candidate catalog is read from disk when the
850
+ candidate is omitted or maps the same `arkRules` files as the project contract (after the loader's
851
+ normalisation, so the project `ark.config.json` passed verbatim counts); otherwise pass
852
+ `candidateArkRuleFiles` in the same shape.
654
853
  It invokes the public classifier directly, is read-only, and marks a blocking result as an MCP
655
854
  error without maintaining separate adapter policy.
package/docs/develop.md CHANGED
@@ -214,7 +214,8 @@ stays plane-free. Empty `planeRoots` in `enforced` mode fails closed
214
214
  (`ARKORDER_MISSING_PLANE`). In-memory. Not durable. Does not replace ArkRun.
215
215
 
216
216
  First freeze is `release()`. Later pattern change is `proposeRelease` then
217
- `apply` (`ARKORDER_UNVALVED_RELEASE`). `refreshSigma`; ingest residual
217
+ `apply` (`ARKORDER_UNVALVED_RELEASE`); `apply` only takes a proposal computed against the
218
+ current Release (`ARKORDER_STALE_PROPOSAL`). `refreshSigma`; ingest residual
218
219
  `absorb | escalate_up | hold` + `reasonCode`; capacity pack as data;
219
220
  `createMemoryReleaseStore`; `ingestTravelAction`; ArkRun `decisionTape`.
220
221