arkgate 4.8.22 → 4.8.24

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 (136) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +27 -78
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +1117 -38
  6. package/bin/ark-mcp-runtime.mjs +412 -138
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +39 -0
  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/agent-projection-command.mjs +5 -1
  13. package/bin/lib/analysis-engine.mjs +15 -11
  14. package/bin/lib/architecture-scan.mjs +50 -6
  15. package/bin/lib/ark-order-doctor.mjs +1 -1
  16. package/bin/lib/ark-order-facts.mjs +410 -15
  17. package/bin/lib/ark-order-invariants.mjs +51 -9
  18. package/bin/lib/ark-order-sensors.mjs +28 -101
  19. package/bin/lib/ark-order-types.mjs +2 -0
  20. package/bin/lib/ark-run-doctor.mjs +14 -0
  21. package/bin/lib/ark-run-facts.mjs +434 -11
  22. package/bin/lib/ark-run-sensors.mjs +5 -3
  23. package/bin/lib/arkgate-pins.mjs +91 -0
  24. package/bin/lib/arkrules-sensors.mjs +50 -249
  25. package/bin/lib/check-args.mjs +23 -1
  26. package/bin/lib/ci-and-commands.mjs +16 -6
  27. package/bin/lib/class-source-scan.mjs +479 -0
  28. package/bin/lib/codex-home.mjs +4 -2
  29. package/bin/lib/config-contract-slices.mjs +701 -0
  30. package/bin/lib/config-contract.mjs +5 -2
  31. package/bin/lib/config-warnings.mjs +32 -0
  32. package/bin/lib/dashboard-cli.mjs +62 -0
  33. package/bin/lib/design-delta.mjs +1 -1
  34. package/bin/lib/diagnostic-catalog.mjs +45 -6
  35. package/bin/lib/doctor-advisories.mjs +84 -1
  36. package/bin/lib/doctor-next-actions.mjs +17 -2
  37. package/bin/lib/doctor-plan.mjs +7 -9
  38. package/bin/lib/effective-contract-load.mjs +121 -59
  39. package/bin/lib/enforcement-honesty.mjs +22 -0
  40. package/bin/lib/enforcement-profiles.mjs +40 -4
  41. package/bin/lib/first-run-help.mjs +3 -2
  42. package/bin/lib/flat-parent-importers.mjs +137 -0
  43. package/bin/lib/flat-parent-pilot.mjs +192 -0
  44. package/bin/lib/gate-files.mjs +29 -2
  45. package/bin/lib/git-change-scope.mjs +109 -0
  46. package/bin/lib/github-enforcement.mjs +1 -1
  47. package/bin/lib/hook-templates.mjs +18 -16
  48. package/bin/lib/host-support-matrix.mjs +3 -1
  49. package/bin/lib/html-report-advisories.mjs +2 -0
  50. package/bin/lib/html-report-depth.mjs +3 -1
  51. package/bin/lib/import-resolve.mjs +13 -1
  52. package/bin/lib/install-migrate.mjs +50 -73
  53. package/bin/lib/invariant-coverage.mjs +59 -11
  54. package/bin/lib/mcp-adoption.mjs +9 -1
  55. package/bin/lib/mcp-hook-payload.mjs +401 -61
  56. package/bin/lib/package-manager.mjs +59 -1
  57. package/bin/lib/passthrough-commands.mjs +83 -0
  58. package/bin/lib/persistence-write-hint.mjs +81 -0
  59. package/bin/lib/physical-cohesion.mjs +16 -18
  60. package/bin/lib/pilot-loop.mjs +21 -8
  61. package/bin/lib/pin-dependent-gates.mjs +117 -0
  62. package/bin/lib/policy-delta-io.mjs +168 -10
  63. package/bin/lib/prepare-change.mjs +120 -6
  64. package/bin/lib/prepare-write.mjs +1 -0
  65. package/bin/lib/presets.mjs +3 -5
  66. package/bin/lib/remediation.mjs +23 -4
  67. package/bin/lib/resolved-candidate-facts.mjs +315 -55
  68. package/bin/lib/rules-inventory.mjs +22 -3
  69. package/bin/lib/rules-under-contract.mjs +179 -5
  70. package/bin/lib/scan-files.mjs +32 -0
  71. package/bin/lib/snippet-analysis.mjs +140 -6
  72. package/bin/lib/source-policy.mjs +34 -0
  73. package/bin/lib/stable-hash.mjs +90 -0
  74. package/bin/lib/start-install-recovery.mjs +71 -1
  75. package/bin/lib/start-preview.mjs +30 -0
  76. package/bin/lib/status-command.mjs +33 -4
  77. package/bin/lib/status-manifest.mjs +32 -4
  78. package/bin/lib/team-parliament-io.mjs +79 -75
  79. package/bin/lib/team-parliament.mjs +6 -1
  80. package/bin/lib/write-path-capabilities.mjs +4 -2
  81. package/dist/chunk-A5EIY3HI.cjs +5 -0
  82. package/dist/chunk-MBXCFLO4.js +16 -0
  83. package/dist/chunk-RNQHRDCQ.cjs +16 -0
  84. package/dist/chunk-SVSQVGFJ.js +5 -0
  85. package/dist/{configTypes-CTELo87-.d.ts → configTypes-BJbaeoj4.d.cts} +53 -4
  86. package/dist/configTypes-BJbaeoj4.d.ts +246 -0
  87. package/dist/eslint/index.cjs +17 -4
  88. package/dist/eslint/index.d.cts +359 -0
  89. package/dist/eslint/index.d.ts +178 -158
  90. package/dist/eslint/index.js +10 -4
  91. package/dist/index.cjs +38 -37
  92. package/dist/index.d.cts +5475 -0
  93. package/dist/index.d.ts +3408 -296
  94. package/dist/index.js +38 -37
  95. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  96. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  97. package/dist/nestjs/index.cjs +1 -18
  98. package/dist/nestjs/index.d.cts +22 -0
  99. package/dist/nestjs/index.d.ts +1 -3
  100. package/dist/nestjs/index.js +1 -18
  101. package/dist/order/index.cjs +1 -1
  102. package/dist/order/index.d.cts +155 -0
  103. package/dist/order/index.d.ts +15 -3
  104. package/dist/order/index.js +1 -1
  105. package/dist/runtime/index.cjs +13 -30
  106. package/dist/{diagnosticCatalog-WD2TPSwZ.d.ts → runtime/index.d.cts} +814 -290
  107. package/dist/runtime/index.d.ts +2678 -108
  108. package/dist/runtime/index.js +13 -30
  109. package/dist/{types-yDNEQ603.d.ts → types-DRinG-vT.d.cts} +576 -140
  110. package/dist/types-DRinG-vT.d.ts +1792 -0
  111. package/docs/README.md +3 -1
  112. package/docs/agent-guide.md +92 -15
  113. package/docs/ai-gates.md +160 -26
  114. package/docs/arkorder.md +32 -15
  115. package/docs/brownfield-adoption.md +5 -1
  116. package/docs/configuration.md +273 -12
  117. package/docs/develop.md +2 -1
  118. package/docs/diagnostics.md +122 -13
  119. package/docs/package-surface.md +31 -8
  120. package/docs/product-voice.md +1 -1
  121. package/package.json +59 -18
  122. package/schemas/ark.analysis-result.schema.json +15 -0
  123. package/schemas/ark.config.schema.json +163 -4
  124. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  125. package/schemas/ark.status-manifest.schema.json +22 -0
  126. package/server.json +3 -9
  127. package/templates/agent-skills/README.md +1 -1
  128. package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
  129. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  130. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  131. package/templates/arkrules/PersistenceAdapters.json +1 -1
  132. package/templates/arkrules/PresentationAdapters.json +1 -1
  133. package/templates/skills/ark-adopt.md +4 -1
  134. package/templates/skills/ark-order.md +4 -2
  135. package/templates/skills/ark-runtime.md +6 -1
  136. package/dist/types-fQ9-KrBj.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.22`; confirm npm with `npm view arkgate@4.8.22 version`; does not close `K01`).
76
+ Current release: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.24`; confirm npm with `npm view arkgate@4.8.24 version`; does not close `K01`).
77
+ Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.23`).
78
+ Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.22`).
77
79
  Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.21`).
78
80
  Prior published: [CHANGELOG](../CHANGELOG.md) (`arkgate@4.8.20`). Plain-English history: [arkgate.online/changelog](https://www.arkgate.online/changelog/).
79
81
  Prior published: [releases/4.8.11.md](releases/4.8.11.md) (`arkgate@4.8.11`).
@@ -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
@@ -438,7 +469,14 @@ clusters per anchor directory (concentration, not volume — dispersed hooks nev
438
469
  fixed corpus-calibrated thresholds; anchors under `app/`/`pages/` are `fixedByConvention` and
439
470
  never move. `reshapePilot.nextPilot` is a **proposed** one-at-a-time card (`moveSample`,
440
471
  `movesTotal`, `successSignal`, `killSwitch`, `doNot[]`) and, when proposed, the single
441
- `pilotLoop` extraction card if no design-weak pattern bet is ahead of it. Run it only via
472
+ `pilotLoop` extraction card if no design-weak pattern bet is ahead of it. A flat file
473
+ at universe level that exactly one child slice imports is a later candidate
474
+ (`doctor.flatParentPilot`, source `flat-parent`): the file, that child, and a
475
+ destination folder inside the child that keeps the file's layer. Two importers,
476
+ or none, produce no card. The sentence is a suggestion with that importer as
477
+ its evidence. It does not change ark-check. `destinationKeepsLayerAndSlice`
478
+ uses the same slice identity as the walls, so `sliceIdentity: "stars"` cannot
479
+ treat two children as one slice. Run a card only via
442
480
  `/ark-loop` through the write gate + atomic preflight; merges are `/ark-adopt` /
443
481
  `/ark-autopilot` judgment cards. `notAScore`, never a verdict/`designFitness` input; there
444
482
  is no apply path.
@@ -618,7 +656,9 @@ Plan by default — there is no `--dry-run` anywhere in `bin/`. `--apply` needs
618
656
  one named rule id: `--promote <ruleId> --apply`, or `--promote=<ruleId>` when
619
657
  the id starts with `-`. It refuses a bare `--promote --apply` rather than
620
658
  rewriting the contract in bulk behind a single flag, refuses an id declared in
621
- two documents rather than silently writing the first, and refuses to make a
659
+ two documents rather than silently writing the first (starter ids are unique:
660
+ `thin-persistence-adapter` / `thin-presentation-adapter`, and a layer that clones
661
+ another layer's archetype gets its ids prefixed with its kebab-cased name), and refuses to make a
622
662
  contract change on a cost this run did not measure. `--promote` cannot be
623
663
  combined with a mode that answers first (`--sensors`, `--coverage`, `--plan`,
624
664
  `--doctor`, …) — that printed the report and exited 0 having written nothing —
@@ -998,6 +1038,13 @@ a `Domain.*` event — the publish throws `ObservedLayerFlowViolationError` befo
998
1038
  reaches history, outbox, or subscribers. Use `'soft'` to record `layer.observedViolation`
999
1039
  trace/audit records without blocking, or `'off'` to disable. Agents should name the event's
1000
1040
  `source` honestly: it is checked against the layer matrix, not just the intent name.
1041
+ Kernels built from `ark.config.json` map intents to layers through `layers[].intentPrefixes`;
1042
+ canonical layer names (`DomainModel`, `ApplicationOrchestration`, … as `ark init` writes them)
1043
+ without prefixes get the built-in ones. A custom-named deny-rule layer with no prefixes is
1044
+ recorded once as a `layer.observedFlowUnresolvable` audit record
1045
+ (`ARKRUN_LAYER_FLOW_UNRESOLVABLE`) and listed by `ark doctor`; pass
1046
+ `enforceObservedLayerFlow: 'hard'` explicitly to make it throw `ArkKernelConfigError`.
1047
+ `peerIsolation` slice walls are not evaluated at runtime (names cannot place a slice).
1001
1048
 
1002
1049
  Strict kernels also require published events to have a registered source intent
1003
1050
  and a matching event contract:
@@ -1143,8 +1190,9 @@ edges are not denied by that rule.
1143
1190
  - **Denied:** `src/features/auth/**` → `src/features/payments/**` (different slice id).
1144
1191
  - **Allowed:** same-slice imports when both paths classify; classic non-peerIsolation denies still apply across layers.
1145
1192
  - **`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.
1146
- - **`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.
1193
+ - **`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.
1147
1194
  - **Fail-closed:** missing paths, empty/unresolvable slice folders, or unclassifiable either side → **deny** via peerIsolation (cannot prove same-slice).
1195
+ - **`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.
1148
1196
  - **`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.
1149
1197
  - **`allowedCrossSlice`** (4.8.4): `[{ "from": "features/checkout", "to": "features/catalog" }]` — one directed slice→slice edge the repo declares on purpose. The reverse still denies.
1150
1198
  - **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).
@@ -1233,8 +1281,14 @@ imports/exports; resolved CLI/preflight is authoritative outside that envelope),
1233
1281
  `ark/no-denied-capabilities` (per-layer capability deny sets),
1234
1282
  `ark/no-arkrun-kernel-in-domain` / `ark/no-arkrun-direct-new` /
1235
1283
  `ark/no-arkrun-transport-bypass` (ArkRun extra; silent when absent; import / `new`
1236
- envelope only), `ark/no-raw-event-publish`, and
1237
- `ark/require-publish-source`. See [ai-gates.md](ai-gates.md).
1284
+ envelope only), `ark/no-arkorder-kernel-in-domain` / `ark/no-arkorder-generic-update`
1285
+ (ArkOrder extra), `ark/arkrules-structure` (ArkRules structure sensors, file-local),
1286
+ `ark/no-raw-event-publish`, and `ark/require-publish-source`. Findings ark-check reports as
1287
+ warnings (type-only placement debt, advisory slice walls, advisory extras) report on the
1288
+ warn-level `ark/architecture-advisory`, so ESLint errors only where ark-check fails (a
1289
+ config without that rule gets them on the blocking rule id, tagged advisory). An
1290
+ invalid contract is one `configInvalid` error per file, not a crash. See
1291
+ [ai-gates.md](ai-gates.md#eslint-editor-feedback--bounded-parity-envelope).
1238
1292
 
1239
1293
  ## Runtime Observability
1240
1294
 
@@ -1316,7 +1370,10 @@ not replace your web framework, HTTP clients, or job scheduler.
1316
1370
  The strongest place to constrain an AI agent is the moment it writes a file, not after.
1317
1371
  `arkgate-mcp` / `ark-mcp` exposes ArkGate over MCP (JSON-RPC over stdio; it prefers a usable
1318
1372
  project TypeScript API, then exact `typescript-ark-host@6.0.3`) so a host can gate
1319
- the write path:
1373
+ the write path. `arkgate mcp` / `ark mcp` starts the same server; that is the form
1374
+ `npx arkgate@<version> mcp --root .` and the MCP Registry use, since npx runs the bin named
1375
+ after the package. Without `--config` the server reads `ark.config.json` when it exists and still
1376
+ starts in a fresh project; an explicit `--config` that points at a missing file exits 1:
1320
1377
 
1321
1378
  For a complete multi-file candidate, use `ark preflight --changes changes.json --json` or MCP
1322
1379
  `ark_prepare_change`. Add `--change-map map.json` (or MCP `changeMap`) only for an explicit schema
@@ -1362,13 +1419,13 @@ The server exposes these thirteen tools. Every tool accepts the additive
1362
1419
  |------|---------------------------|
1363
1420
  | `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. |
1364
1421
  | `ark_manifest` | No non-project args: return the machine-readable architecture contract with an authoritative binding after the identity handshake. |
1365
- | `validate_code` | `{ source, layer?, filePath? }`: validate one snippet; infer the layer from `filePath` when possible; return an error result when invalid. |
1422
+ | `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`. |
1366
1423
  | `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. |
1367
- | `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. |
1424
+ | `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`. Weakening / new layer / new allow edge needs `adrPath` on the acknowledgement. |
1368
1425
  | `ark_coverage` | No args: report per-layer counts, every unclassified file, unmatched layers, and missing rule edges. |
1369
- | `ark_place` | `{ filePath?, description? }`: resolve or propose a governed home and return its import/global constraints. |
1370
- | `ark_prepare_write` | `{ source, filePath?, description?, layer? }`: compose placement and snippet validation, with hashes and a mechanical-safe patch when available. |
1371
- | `ark_prepare_change` | `{ changes, changeMap? }`: preflight one complete create/update/delete batch in memory; never writes files. |
1426
+ | `ark_place` | `{ filePath, description? }`: resolve the governed home for `filePath` (fail-closed without it; never invents a path) and return its import/global constraints. |
1427
+ | `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`. |
1428
+ | `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. |
1372
1429
  | `ark_recommend` | No args: return the deterministic application-shape plan used by `ark-check --recommend --json`. |
1373
1430
  | `ark_suggest_include` | No args: propose TypeScript/JavaScript include roots from workspaces and nested packages. |
1374
1431
  | `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. |
@@ -1398,7 +1455,13 @@ Every project-bound tool success, tool error, and JSON-RPC error data carries:
1398
1455
  ```
1399
1456
 
1400
1457
  `projectId` stays stable across process restarts and contract edits; `runtimeId` and
1401
- `processStartedAt` identify this live process. Binding states are:
1458
+ `processStartedAt` identify this live process. The contract (`ark.config.json`, the project
1459
+ manifest, referenced ArkRules files) is loaded once at startup: after any of them changes on
1460
+ disk, every project tool except `ark_identity` returns `CONTRACT_STALE` (`isError`,
1461
+ `authoritative: false`, `contractStale: true`) until the MCP server restarts — the process never
1462
+ answers authoritatively from an old contract. `ark_identity` keeps answering with
1463
+ `contractStale: true` so the host can diagnose it; the project-local CLI reads the current
1464
+ contract meanwhile. Binding states are:
1402
1465
 
1403
1466
  - `matched` — canonical `expectedRoot` is the exact project root, or it is a contained
1404
1467
  descendant and the caller also supplied the matching project id; `authoritative` is `true`;
@@ -1505,8 +1568,22 @@ doctor → compact router (and `/ark-autopilot` only after the skill pack).
1505
1568
  | `GET /workflows` | Workflows monitor: counts + `workflows[]` summaries (`id`, `name`, `status`, optional `currentStep` / `error`) |
1506
1569
 
1507
1570
  Dual package bins **`ark-dashboard`** and **`arkgate-dashboard`**
1508
- (`bin/ark-dashboard.mjs`) poll `--url` (default `http://127.0.0.1:3000/snapshot`)
1509
- and sibling `/outbox` + `/workflows` on an interval (`--interval`, 200–60000 ms).
1571
+ (`bin/ark-dashboard.mjs`) poll `--url` and sibling `/outbox` + `/workflows` on an
1572
+ interval (`--interval` / `--timeout`, clamped to 200–60000 ms). `--url` accepts the
1573
+ inspector root (`handle.url`) or `handle.snapshotUrl`. `startInspector()` binds a
1574
+ **random port** unless you pass `{ port }`, so pass the URL your app printed (or set
1575
+ `ARK_DASHBOARD_URL`); the fallback `http://127.0.0.1:3000/snapshot` only matches
1576
+ `startInspector({ port: 3000 })`, and the dashboard prints that hint when it cannot
1577
+ connect. `--once` renders one frame and exits (non-zero when unreachable);
1578
+ `--help` / `--version` exit 0; unknown flags exit 2 with one line.
1579
+
1580
+ The default in-memory event buffer (`InMemoryEventBuffer`) is capped at
1581
+ `maxHistorySize` (default 1000) and its records are marked `dispatched` once the kernel
1582
+ finishes local delivery, so `/outbox` shows real backlog only. Broker sends are marked
1583
+ `dispatched` / `failed` from the handoff outcome, and a failed handoff (awaited or
1584
+ fire-and-forget) records an `event.handoffFailed` trace + audit entry. An injected
1585
+ `eventBuffer` stays relay-owned: records remain `pending` for your relay's `claim` /
1586
+ `markDispatched` (except broker handoffs the kernel performs itself).
1510
1587
  ANSI escape sequences + polling only — no React, Ink, or Blessed. Use
1511
1588
  `ark dashboard` / `arkgate dashboard` (passthrough to `bin/ark-dashboard.mjs`) or the
1512
1589
  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
+ ```
782
+
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
+ ```
754
838
 
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.
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,13 @@ 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
+ Or use the repository's composite Action at a pinned release or commit. Pin the tag (or commit
945
+ SHA) that matches the `arkgate` version in your `package.json` and write hook. The Action runs the
946
+ checker from the ref you pin, so an older ref runs an older checker, and that checker rejects newer
947
+ config keys (for example `schemaVersion` 1.3, `childSlices`, `sliceIdentity`) with exit 2:
826
948
 
827
949
  ```yaml
828
- - uses: pedroknigge/arkgate@v3.7.0
950
+ - uses: pedroknigge/arkgate@v4.8.24 # same version as the arkgate devDependency
829
951
  with:
830
952
  root: .
831
953
  config: ark.config.json
@@ -912,6 +1034,18 @@ npx ark-check --update-baseline # writes .ark-baseline.json — commit it
912
1034
  npx ark-check --baseline # only NEW violations fail
913
1035
  ```
914
1036
 
1037
+ `--update-baseline` always measures the whole governed tree. It refuses `--changed` /
1038
+ `--local` and ignores `ARK_CHECK_LOCAL=1`, because a changed-files scan would overwrite the
1039
+ freeze with only that subset.
1040
+
1041
+ `ark-check --report` applies the committed `.ark-baseline.json` by default (the same file
1042
+ `--doctor` reads), so the report, `.ark/reports/latest.json`, and `ark status` agree with
1043
+ doctor on what is active versus frozen. A plain `ark-check` without `--baseline` is
1044
+ unchanged, and so is a merge verdict: with `--strict` / `--strict-merge` / `--contract-diff`,
1045
+ `--report` does **not** apply the implicit freeze (pass `--baseline` explicitly), so adding a
1046
+ reporting flag can never turn a failing merge run green. Only `--report` refreshes the `ark status` last-check snapshot; a plain check or
1047
+ `--doctor` does not write it.
1048
+
915
1049
  `--update-baseline` also patches existing check **invocations** so CI keeps using the
916
1050
  new freeze file: `package.json` scripts and GitHub workflow `run:` lines that already
917
1051
  call `ark-check` / `arkgate-check` via `npx`, `pnpm`, `yarn`, `npm`, or `node`. Those
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 /