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/CHANGELOG.md CHANGED
@@ -96,6 +96,97 @@ in the immutable pre-2.0 archive linked below.
96
96
  Existing configs keep whatever URL they already have. Editor completion
97
97
  matches the 4.x line you just installed.
98
98
 
99
+ ## 4.8.24 — 2026-09-29
100
+
101
+ **Patch** over **4.8.23**. Follow-ups to the hierarchical slices shipped in 4.8.23
102
+ (issues #335–#338), every defect a full audit of the gate and its add-ons
103
+ confirmed, and much lower memory per validation (#339). **Write. Check. Ship.**
104
+ **No required config migration:** a config that loads in 4.8.23 still loads;
105
+ ids that changed meaning warn (`CONFIG_CHILD_SLICES_INERT`,
106
+ `CONFIG_SLICE_LEGACY_STARS_ID`) and keep their 4.8.23 verdict for one release.
107
+ No `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
108
+ `ark.config.json` still does **not** turn `arkOrder` on.
109
+
110
+ **Minimum version 4.8.24 for the new config fields** (`sharedImportsSlice`
111
+ object form with `stopAt`, `childSlices.message`, `childSlices.siblings.ratchet`).
112
+ arkgate 4.8.23 and older reject them at config load. Pin 4.8.24 in the write
113
+ hook and CI first.
114
+
115
+ **Status: released** (tag `v4.8.24`). This file does not assert registry
116
+ state; run `npm view arkgate@4.8.24 version` to confirm (#314).
117
+
118
+ ### Added
119
+ - `sharedImportsSlice` accepts `{ "mode": "deny-cross-parent", "stopAt": [...] }`. The whole-graph `CROSS_PARENT_VIA_SHARED` walk never starts at or passes through a stop file, so a declared composition root (`kernel/bootstrap.ts`, `kernel/registrations/**`) is no longer reported as a crossing. `stopAt` matches like a shared root or an alias glob; a stop that covers the whole tree or a whole layer root is rejected. Findings carry `via`, and doctor `sharedWalkHubs` names a likely undeclared composition root. Adding stops is a weakening policy delta. The string forms are unchanged. arkgate 4.8.23 and older reject the object (issue [#335](https://github.com/pedroknigge/arkgate/issues/335)).
120
+ - Optional `childSlices.message` sets the text for inner-wall findings (`CROSS_SIBLING_SLICE`, universe common code importing a child). Without it ArkGate uses its own text; the rule `message` stays the universe-wall text and is never reused on inner-wall findings. Same text in ark-check, CI, the write hook, MCP, and ESLint (issue [#337](https://github.com/pedroknigge/arkgate/issues/337)).
121
+ - Optional `childSlices.siblings.ratchet`: `true` always fails a new advisory crossing, `false` only measures (issue [#336](https://github.com/pedroknigge/arkgate/issues/336)).
122
+ - `arkgate mcp` / `ark mcp` start the stdio MCP server (same as `arkgate-mcp`).
123
+ - ESLint: warn-level `ark/architecture-advisory` rule (findings ark-check reports as warnings) and `ark/arkrules-structure` (ArkRules structure sensors on the same Effective Contract).
124
+ - New advisory `ARKRULE_FILE_UNREFERENCED` for an `arkrules/*.json` the config does not reference, and `doctor.rulesMigration`.
125
+ - `ark-dashboard --help`, `--version`, `--once`, `ARK_DASHBOARD_URL`.
126
+ - `npm run bench:memory` regression guard (budgets in `eval/performance/memory-budgets.v1.json`).
127
+
128
+ ### Changed
129
+ - **Memory and speed.** ark-check, `--doctor`, the write hook, ApplyPatch preflight, the MCP server and the ESLint plugin use much less memory; verdicts are byte-identical. On a 20k-file repo: ark-check peak 422 → 303 MB (3.7 → 2.3 s), ApplyPatch hook 649 → 414 MB (9.5 → 3.6 s), Write hook 163 → 146 MB (0.68 → 0.24 s), MCP server after 50 calls 725 → 442 MB. Sources are read one at a time and released, the facts hash streams, module resolution is memoized per run, the Write/Edit hook no longer walks the whole `include` tree for tsconfig aliases, and preflight reuses base parses.
130
+ - Advisory sibling crossings are advisory on day 1. The anti-growth ratchet is judged per rule and turns on only when the baseline in use already records an advisory sibling crossing of that rule (issue [#336](https://github.com/pedroknigge/arkgate/issues/336)).
131
+ - `CONFIG_CHILD_SLICES_VERSION` (and the other new-key version warnings) fire only with evidence that the repo pins an older arkgate (exact `package.json` dependency, installed copy, lockfile, hook script, or CI workflow). The finding points at that file and line and names the version to bump to (issue [#338](https://github.com/pedroknigge/arkgate/issues/338)).
132
+ - `childSlices` on a rule without `peerIsolation: true` and `allowed: false` still loads (as in 4.8.23) but is inert; ark-check now warns `CONFIG_CHILD_SLICES_INERT` and says how to fix it (add both flags to the rule, or remove `childSlices`). No config that loads in 4.8.23 fails to load.
133
+ - `sliceIdentity: "stars"` keeps every star binding (`modules/*/api/*` binds `orders/api/v1`, not `api/v1`). **Migration:** a `sliceAliases` `to` or an `allowedCrossSlice` / `childSlices.allowedCrossSlice` entry written against the 4.8.23 id (`api/v1/x`, `api/v1`) keeps loading and keeps its 4.8.23 verdict for this release: the alias joins the universe whose old id it names, and the allowance still clears the edge it cleared. ark-check warns `CONFIG_SLICE_LEGACY_STARS_ID` and names the new id to write (`orders/api/v1/x`). A legacy alias target is rejected at config load only when it maps to more than one universe shape of the rule. The legacy form is removed in the next minor release.
134
+ - `childSlices.commonFolders` counts only a folder directly under the universe; the same name inside a child belongs to that child.
135
+ - The write hook fails closed: if it cannot run inside an Ark project (invalid config, missing ArkRules file, missing `dist/`), governed writes are denied with exit 2 (`WRITE_GATE_UNAVAILABLE`) instead of exit 1, which hosts let through. The hook finds its project root like ark-check: a set `--root-env` variable, else the root that holds `ark.config.json`, else it walks up from the payload (written file, `cwd`, `workspace_roots`) to the nearest `ark.config.json`. A write outside any Ark project (no `ark.config.json` found) is allowed, as in 4.8.23.
136
+ - `ark start --apply --remove-host <host>` removes only the Ark hook entries from a merged `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json` or `.agents/hooks.json` (same identity as the merge) and keeps every user key and hook; the file is deleted only when nothing else remains. "customized" is reported only when the Ark entry itself was edited.
137
+ - An enforced invariant with `coverage: { test: false }` and no `coverage.symbol` is uncovered; `coverage.symbol: "Class.member"` needs the member declared in that class.
138
+ - ArkOrder `apply()` only accepts a proposal bound to the current Release (`ARKORDER_STALE_PROPOSAL`); `restore()` never changes ξ or rolls the version back; `ReleaseStore.load()` is validated; the plane advances only after `save()`; `informationBudget` is enforced when a Release is created.
139
+ - ArkRun: the default event buffer is capped and settles after local delivery; broker handoff failures are recorded (`event.handoffFailed`); config kernels with canonical layer names enforce hard observed-layer-flow.
140
+ - `ark-check --update-baseline` always freezes the whole tree and refuses `--changed` / `--local`.
141
+
142
+ ### Fixed
143
+ - `--changed`, `--local`, `--against` and `--persona` no longer give a false green: paths resolve relative to `--root` (monorepo packages), non-ASCII names are kept, large or failed git listings exit 2, and a missing base ref exits 2 (`changed-needs-base`) instead of "nothing changed".
144
+ - `--changed` catches a new `CROSS_PARENT_VIA_SHARED` created by editing only a shared-root file, and no longer warns `CONFIG_LAYER_PATTERN_NO_MATCHES` for untouched layers.
145
+ - `ark-check --report`, `latest.json`, `--doctor` and `ark status` agree on frozen versus active findings.
146
+ - Write path parity with CI: the hook, `validate_code`, `ark_prepare_write`, `ark_prepare_change` and ApplyPatch preflight enforce ArkRules structure sensors and ArkOrder editor sensors, load ArkRules like ark-check, handle realpath aliases, and accept the full Codex `apply_patch` format.
147
+ - MCP: `validate_code` / `ark_check` output schemas pass MCP SDK validation (tools no longer disappear); a stale contract returns `CONTRACT_STALE`; `ark_status` honors `--config`; `arkgate-mcp --help` / `--version` exit.
148
+ - MCP Registry: `server.json` runs `npx arkgate@<version> mcp --root .`; the old descriptor never started the server.
149
+ - CommonJS TypeScript consumers: `.d.cts` declarations ship and are mapped for every entry.
150
+ - ESLint: warnings in ark-check are warnings in ESLint; messages carry the rule message and slice reason; ArkOrder rules show real text at 1-based columns; tsconfig JSONC / nearest tsconfig / `extends` are read correctly; an invalid config reports one diagnostic instead of crashing; `require('arkgate/eslint')` returns the plugin (it also carries a non-enumerable `__esModule`, so Babel `_interopRequireWildcard` and TypeScript `__importStar` still see every named export); `--cache` invalidates on contract change; forbidden-globals catches destructuring and bracket access.
151
+ - Slices: adapter diagnostics carry `reasonId` and the universe pair; `CROSS_PARENT_VIA_SHARED` honors the universe `allowedCrossSlice`; fail-closed reasons are named in the hook and MCP; folder-form `sliceAliases` cover their subtree; the flat-parent move card counts every resolved importer; `SHARED_IMPORTS_SLICE` is catalogued.
152
+ - Setup: `ark start --archetype/--preset` writes that shape; re-running `ark start --apply` works; `--remove-host` does not touch packages; existing `.claude/settings.json` / `.codex/hooks.json` are merged instead of skipped or wiped; `typecheck` is added only when TypeScript is present; the steward lock holds under `--strict-merge --contract-session`; `ark status --config` / `ark agents-md --config` work; starter ArkRules ids are unique.
153
+ - ArkRules: policy delta reads base ArkRules from the base ref (demoting an enforced rule is weakening); class-shape sensors see generic classes, ignore braces in strings and regexes, and skip a `static { … }` initialization block (it is not a public member); `ArkRulesValidationError` is exported from the root.
154
+ - ArkRun: undeclared emit/handle/depend count only kernel-traced receivers (`res.send`, `require.resolve`, rxjs `subscribe` are ignored); typed `send(Creator)` resolves names; `ArkModule.forRoot()` is a composition root; `arkgate/runtime` and `arkgate/nestjs` share one kernel chunk (`instanceof` works). A namespace import of a composition root (`import * as main from '../main'; main.ark.publisher(..)`) is traced as a kernel receiver, and an untraced receiver counts when its name carries one of the project's effective intent prefixes (configured `layers[].intentPrefixes` plus the canonical ones), not only `Domain.`/`Application.`….
155
+ - ArkOrder: `ARKORDER_XI_FIELD_WRITE` recognizes Prisma/ORM clients by construction, `GENERIC_UPDATE` / `INGEST_WRITES_XI` false positives removed, `xi-ttl` and `information-budget` sensors emit, NaN/Infinity rejected, documented types exported, and the shipped `bin/lib/ark-order-invariants.mjs` loads.
156
+ - Docs: the composite Action pin tracks the current release, and configuration.md names the real policy-delta entry points.
157
+ - Generated CI workflows, host hooks and MCP entries in a project that does not
158
+ have arkgate as a local dependency run `npx -y -p arkgate@<exact version> <bin>`
159
+ instead of a bare `npx ark-check` / `npx arkgate-mcp` (npm resolved those as a
160
+ nonexistent package, or ran a stale global). Without a `package.json` the workflow
161
+ skips the install step and the npm cache. `--migrate-commands` rewrites bare runners
162
+ into the pinned form, and `--doctor` leads with `PACKAGE_PIN_ABSENT` when a generated
163
+ file still uses the local runner without the pin.
164
+ - `ark-check --json` answers every fatal error with a JSON envelope (`POLICY_BASE_UNREADABLE` when a CI base ref such as `GITHUB_BASE_REF` cannot be read in the checkout, otherwise `CHECK_ERROR`), not only `CONFIG_INVALID`; exit code stays 2.
165
+ - Security: CodeQL `js/polynomial-redos` in the steward email check and two incomplete-sanitization alerts are closed.
166
+
167
+ ## 4.8.23 — 2026-09-28
168
+
169
+ **Patch** over **4.8.22**. Hierarchical slices: the issue #326 series
170
+ (#327–#333). **Write. Check. Ship.** **No required config migration.** Every
171
+ new field is optional; absent keeps today's output. No `schemaVersion` bump.
172
+ Does not close `K01` / `Z09`. This mother `ark.config.json` still
173
+ does **not** turn `arkOrder` on.
174
+
175
+ **Minimum version 4.8.23 for the new config fields.** arkgate 4.8.22 and older
176
+ reject `childSlices` and `sharedImportsSlice: "deny-cross-parent"` at config
177
+ load. Pin 4.8.23 in the write hook and CI first.
178
+
179
+ **Status: released** (tag `v4.8.23`). This file does not assert registry
180
+ state; run `npm view arkgate@4.8.23 version` to confirm (#314).
181
+
182
+ ### Added
183
+ - Optional `childSlices` on a `peerIsolation` rule: an inner wall under today's universe wall. Absent, behavior and output stay the same. Present, a cross-universe deny is `reasonId` `CROSS_PARENT_SLICE` (never advisory) and a sibling crossing is `CROSS_SIBLING_SLICE` (`siblings`: `deny` or `advisory`). `commonFolders` and flat files are universe common. `parentMayImportChild` defaults to false. The baseline key does not include `reasonId`. Advisory siblings still fail when they grow past the recorded baseline, and `productHonesty.finished` stays false while any child wall is advisory. Doctor reports `{ crossParent, crossSibling, pairs }`. arkgate 4.8.22 and older reject the key. Wildcards and aliases are not part of that introduction (#328).
184
+ - `sharedImportsSlice` may be `"deny-cross-parent"`. The direct shared-root hop stays a `SHARED_IMPORTS_SLICE` warning. ark-check and CI add `reasonId` `CROSS_PARENT_VIA_SHARED` when a slice reaches another universe only through a shared root. A shared hop inside one universe is not a finding. `"deny"` is unchanged. The write hook and ESLint see one edge at a time and do not block the path. The baseline key does not include `reasonId`. arkgate 4.8.22 and older reject the value at config load. Does not close `K01` (#329).
185
+ - `childSlices.siblings` may be `{ "default": "advisory" | "deny", "enforce": [...] }`. A `CROSS_SIBLING_SLICE` whose importer is an enforced child id or subtree path is an error. Other sibling crossings stay warnings, and a new one past the recorded baseline still fails. `default: "deny"` denies every sibling crossing. The string forms `"deny"` and `"advisory"` stay the same. `ruleId` stays `LAYER_IMPORT_VIOLATION`. The baseline key does not include `reasonId`. `productHonesty.finished` stays false while `default` is advisory. An enforce list does not finish the house. The write hook, ESLint, ark-check, and CI all see the per-edge severity. A build that still types `siblings` as a string enum rejects the object at config load. Does not close `K01` (#330).
186
+ - `childSlices.allowedCrossSlice` accepts a whole-segment `*` (`features/projects/*`). It clears only a `CROSS_SIBLING_SLICE` inside one universe. It does not clear `CROSS_PARENT_SLICE` or `CROSS_PARENT_VIA_SHARED`. A pattern that can name two universes warns `CONFIG_CHILD_SLICE_CROSS_UNIVERSE` and the cross-parent finding still fires. A bare name is rejected at config load. `**` and a partial segment are rejected. The universe `allowedCrossSlice` still treats `*` as a literal. Adding an entry is a weakening policy delta; a wildcard that widens a literal entry is weakening too. `ruleId` stays `LAYER_IMPORT_VIOLATION`. The baseline key does not include `reasonId`. The write hook, ESLint, ark-check, and CI all see the per-edge decision. A build that still rejects unknown `childSlices` fields rejects the key at config load. arkgate 4.8.22 and older reject `childSlices`. Does not close `K01` (#331).
187
+ - `childSlices.sliceAliases` maps a folder outside the slice trees onto a child slice (`{ "from": "lib/compliance/**", "to": "features/projects/compliance" }`). The file takes that universe id and that child id for both walls, so an import into another universe is `CROSS_PARENT_SLICE`. A bare name, a universe id alone, an unknown universe, and a wildcard in `to` are rejected at config load. A glob that overlaps a slice folder is rejected. Two aliases that match the same file are rejected. Doctor lists each alias as an owed move (files, target slice, destination folder) and does not count the files as finished. Adding aliases is a strengthening policy delta. Removing them is weakening. Additions and removals together, including a retarget, need a judgment. `ruleId` stays `LAYER_IMPORT_VIOLATION`. The baseline key does not include `reasonId`. The write hook, ESLint, snippet analysis, ark-check, and CI all see the alias. A build that still rejects unknown `childSlices` fields rejects the key at config load. arkgate 4.8.22 and older reject `childSlices`. Does not close `K01` (#332).
188
+ - Doctor suggests a move for a flat file that sits at universe level when exactly one child slice imports it (`doctor.flatParentPilot`). The card names the file, that child, and a destination folder inside the child that keeps the file's layer. Two importers, or none, produce no card. The sentence is a suggestion. It does not change ark-check, `ruleId`, `reasonId`, or the baseline key. The reshape destination check uses the same slice identity as the walls, so `sliceIdentity: "stars"` does not treat two children as one slice. No new config field. Does not close `K01` (#333).
189
+
99
190
  ## 4.8.22 — 2026-09-27
100
191
 
101
192
  **Patch** over **4.8.21**. Daily accumulate (2026-09-27 ART) of the ships
package/README.md CHANGED
@@ -25,11 +25,11 @@ Design the boundary before you implement. Put a small interface in front of the
25
25
 
26
26
  ## Try it
27
27
 
28
- One command. Preview only — no files change. Current release is **4.8.22**
28
+ One command. Preview only — no files change. Current release is **4.8.24**
29
29
  (check npm `latest` with `npm view arkgate version`):
30
30
 
31
31
  ```bash
32
- npx arkgate@4.8.22 start
32
+ npx arkgate@4.8.24 start
33
33
  ```
34
34
 
35
35
  ## What a red result looks like
@@ -320,9 +320,11 @@ queue monitors:
320
320
 
321
321
  Poll those facts from the dual bins **`ark-dashboard`** / **`arkgate-dashboard`**
322
322
  (`bin/ark-dashboard.mjs`). ANSI + polling only (no React/Ink/Blessed). Point
323
- `--url` / `-u` at the inspector snapshot (default
324
- `http://127.0.0.1:3000/snapshot`); the dashboard also fetches sibling `/outbox`
325
- and `/workflows`. `--interval` / `-i` is clamped to 200–60000 ms (default 2000).
323
+ `--url` / `-u` at the inspector (`handle.url` or `handle.snapshotUrl`); the dashboard
324
+ also fetches sibling `/outbox` and `/workflows`. `startInspector()` binds a random port
325
+ unless you pass `{ port }`, so the fallback `http://127.0.0.1:3000/snapshot` (or
326
+ `ARK_DASHBOARD_URL`) only matches `startInspector({ port: 3000 })`. `--interval` / `-i`
327
+ is clamped to 200–60000 ms (default 2000); `--once`, `--help`, `--version` are supported.
326
328
  Also available as `ark dashboard` / `arkgate dashboard` (passthrough to the same bin). Kernel stays
327
329
  JSON-only; presentation stays in `bin/`.
328
330
 
@@ -66,6 +66,7 @@ import {
66
66
  printAdrPresenceHint,
67
67
  } from './lib/doctor-plan.mjs';
68
68
  import { runRatchetCores } from './lib/core-ratchet.mjs';
69
+ import { applyAdvisorySiblingRatchet } from './ark-layer-match.mjs';
69
70
  import {
70
71
  baselineKey,
71
72
  baselineOccurrenceKeys,
@@ -105,7 +106,7 @@ import {
105
106
  normalize,
106
107
  walk,
107
108
  } from './lib/scan-files.mjs';
108
- import { configWarning } from './lib/config-warnings.mjs';
109
+ import { configInvalidJsonEnvelope, configWarning } from './lib/config-warnings.mjs';
109
110
  import { runArchitectureScan } from './lib/architecture-scan.mjs';
110
111
  import {
111
112
  ANALYSIS_COMPLETENESS,
@@ -123,6 +124,7 @@ import {
123
124
  applyAgainstRatchet,
124
125
  bindTeamBaseRefs,
125
126
  contractSessionFrom,
127
+ pruneScopedPatternWarnings,
126
128
  runTeamPreflight,
127
129
  ungovernedDumpMessage,
128
130
  } from './lib/team-parliament-io.mjs';
@@ -134,7 +136,9 @@ import {
134
136
  resolveEffectiveProjectRoot,
135
137
  } from './lib/project-root.mjs';
136
138
  import { demoteArkRuleTeethUnderClassificationFloor } from './lib/rules-under-contract.mjs';
137
- import { localCheckEnvelope, parseArgs, resolveDesignDeltaBaseRef } from './lib/check-args.mjs';
139
+ import {
140
+ effectiveBaselineName, localCheckEnvelope, parseArgs, resolveDesignDeltaBaseRef, UPDATE_BASELINE_SCOPE_MESSAGE,
141
+ } from './lib/check-args.mjs';
138
142
  import { detectConfig, proposeForUncovered } from './lib/check-config-detect.mjs';
139
143
  import { runWatchMode } from './lib/check-watch.mjs';
140
144
 
@@ -1332,6 +1336,7 @@ async function main() {
1332
1336
  strictMerge: args.strictMerge || args.contractDiff,
1333
1337
  basePath: args.policyBase,
1334
1338
  baseRef: args.policyBaseRef,
1339
+ adoptMissingBase: args.policyBaseFromTeam === true,
1335
1340
  acknowledgementPath: args.policyAck,
1336
1341
  });
1337
1342
  const preflight = runTeamPreflight({
@@ -1446,6 +1451,7 @@ async function main() {
1446
1451
  args,
1447
1452
  });
1448
1453
 
1454
+ if (args.changed) pruneScopedPatternWarnings(warnings, root, loadGovernedFiles);
1449
1455
  // Align merge with extraMergeTeeth stamp: demote enforced ArkRules under classification floor.
1450
1456
  const preCov = computeCoverage(root, config, files, rules);
1451
1457
  const populatedLayerCount = Array.isArray(preCov.layers)
@@ -1514,6 +1520,8 @@ async function main() {
1514
1520
  }
1515
1521
 
1516
1522
  if (args.updateBaseline) {
1523
+ // A persona can turn on --changed after parseArgs; a subset scan must never rewrite the baseline.
1524
+ if (args.changed) { console.error(UPDATE_BASELINE_SCOPE_MESSAGE); process.exitCode = 2; return; }
1517
1525
  if (!contractSessionFrom(args)) {
1518
1526
  console.error('Growing the baseline requires --contract-session. Freeze in a law-only PR.');
1519
1527
  process.exitCode = 1;
@@ -1586,15 +1594,18 @@ async function main() {
1586
1594
  changed: args.changed,
1587
1595
  changedPaths,
1588
1596
  occurrenceKeys: baselineOccurrenceKeys(violations),
1597
+ rules,
1598
+ layers: config.layers,
1589
1599
  });
1590
1600
  activeViolations = ratcheted.activeViolations;
1591
1601
  suppressed = ratcheted.suppressed;
1592
- } else if (args.baseline) {
1593
- const baseline = readBaseline(root, args.baseline);
1602
+ } else if (effectiveBaselineName(args, root)) {
1603
+ const baseline = readBaseline(root, effectiveBaselineName(args, root));
1594
1604
  if (baseline.exists) {
1595
1605
  const occurrenceKeys = baselineOccurrenceKeys(violations);
1596
- suppressed = violations.filter((_, index) => baseline.keys.has(occurrenceKeys[index]));
1597
- activeViolations = violations.filter((_, index) => !baseline.keys.has(occurrenceKeys[index]));
1606
+ const judged = applyAdvisorySiblingRatchet(violations, occurrenceKeys, baseline.keys, { rules, layers: config.layers });
1607
+ suppressed = judged.filter((_, index) => baseline.keys.has(occurrenceKeys[index]));
1608
+ activeViolations = judged.filter((_, index) => !baseline.keys.has(occurrenceKeys[index]));
1598
1609
  const currentKeys = new Set(occurrenceKeys);
1599
1610
  staleBaselineKeys = [...baseline.keys].filter((key) => !currentKeys.has(key)).length;
1600
1611
  } else {
@@ -1637,81 +1648,16 @@ async function main() {
1637
1648
  }
1638
1649
 
1639
1650
  if (args.rulesInventory) {
1640
- const { buildRulesInventory, inventoryToExtractionCard } = await import('./lib/rules-inventory.mjs');
1641
- const fileContents = {};
1642
- const fileLayers = {};
1643
- for (const file of files.slice(0, 400)) {
1644
- const rel = normalize(path.relative(root, file));
1645
- try {
1646
- fileContents[rel] = fs.readFileSync(file, 'utf8');
1647
- const layer = layerForFile(root, file, config.layers);
1648
- if (layer) fileLayers[rel] = layer;
1649
- } catch {
1650
- /* skip unreadable */
1651
- }
1652
- }
1653
- const contracted = [];
1654
- if (config.arkRules) {
1655
- try {
1656
- const { loadEffectiveArkRulesFromDisk } = await import('./lib/effective-contract-load.mjs');
1657
- const loaded = loadEffectiveArkRulesFromDisk(root, config);
1658
- for (const rule of loaded.arkRules.structure ?? []) contracted.push(rule.id);
1659
- for (const inv of loaded.arkRules.invariants ?? []) contracted.push(inv.id);
1660
- } catch {
1661
- /* advisory inventory still useful */
1662
- }
1663
- }
1664
- const inventory = buildRulesInventory({
1665
- fileContents,
1666
- fileLayers,
1667
- layerContexts: (config.layers ?? []).map((layer) => ({
1668
- name: layer.name,
1669
- intentPrefixes: layer.intentPrefixes ?? [],
1670
- })),
1671
- contractedRuleIds: contracted,
1672
- });
1673
- const nextPilot =
1674
- inventory.candidates[0] != null
1675
- ? inventoryToExtractionCard(inventory.candidates[0])
1676
- : null;
1677
- let evidenceLines = [];
1678
- let coverageEvidence = null;
1679
- if (config.arkRules && Object.keys(config.arkRules).length > 0) {
1680
- try {
1681
- const { summarizeRulesUnderContract, formatArkRulesEvidenceLines } = await import(
1682
- './lib/rules-under-contract.mjs'
1683
- );
1684
- const section = summarizeRulesUnderContract(root, config, {
1685
- files: files.map((file) => ({ path: normalize(path.relative(root, file)) })),
1686
- });
1687
- evidenceLines = formatArkRulesEvidenceLines(section);
1688
- if (section.active === true) {
1689
- coverageEvidence = {
1690
- symbolEvidence: section.symbolEvidence ?? [],
1691
- discarded: section.coverageStats?.discarded ?? null,
1692
- };
1693
- }
1694
- } catch {
1695
- /* inventory still useful when coverage cannot be read */
1696
- }
1697
- }
1698
- const payload = {
1699
- rulesInventory: inventory,
1700
- rulesMigration: {
1701
- inventoried: inventory.inventoried,
1702
- underContract: inventory.underContract,
1703
- frozen: inventory.frozen,
1704
- notAScore: true,
1705
- },
1706
- nextPilot: nextPilot,
1707
- ...(coverageEvidence ? { coverageEvidence } : {}),
1708
- };
1651
+ // Same helper as MCP ark_rules_inventory and doctor.rulesMigration (AR15).
1652
+ const { buildRulesInventoryPayload, formatRulesMigrationCounts } = await import(
1653
+ './lib/rules-under-contract.mjs'
1654
+ );
1655
+ const { payload, evidenceLines } = buildRulesInventoryPayload(root, config, files);
1656
+ const { rulesInventory: inventory, nextPilot } = payload;
1709
1657
  if (args.json) {
1710
1658
  console.log(JSON.stringify(payload, null, 2));
1711
1659
  } else {
1712
- console.log(
1713
- `Rules inventory: ${inventory.inventoried} inventoried, ${inventory.underContract} under contract, ${inventory.frozen} frozen (not a score).`
1714
- );
1660
+ console.log(`Rules inventory: ${formatRulesMigrationCounts(payload.rulesMigration)}.`);
1715
1661
  for (const c of inventory.candidates.slice(0, 12)) {
1716
1662
  console.log(` - [${c.confidence}] ${c.kind} @ ${c.file}:${c.line} — ${c.message}`);
1717
1663
  }
@@ -1863,6 +1809,7 @@ async function main() {
1863
1809
  ? renderBeginnerHtmlReport(reportPayload)
1864
1810
  : renderHtmlReport(reportPayload);
1865
1811
  const reportPath = path.isAbsolute(args.report) ? args.report : path.join(root, args.report);
1812
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
1866
1813
  fs.writeFileSync(reportPath, html);
1867
1814
 
1868
1815
  const archive = archiveReportSnapshots(root, {
@@ -2085,6 +2032,8 @@ async function main() {
2085
2032
  }
2086
2033
 
2087
2034
  main().catch((error) => {
2035
+ const envelope = configInvalidJsonEnvelope(error, process.argv);
2036
+ if (envelope) console.log(envelope);
2088
2037
  console.error(
2089
2038
  process.env.ARK_DEBUG_STACK === '1' && error instanceof Error
2090
2039
  ? error.stack
@@ -1,43 +1,55 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
2
3
  import { parseArgs } from 'node:util';
4
+ import {
5
+ DASHBOARD_DEFAULT_INTERVAL_MS,
6
+ DASHBOARD_DEFAULT_TIMEOUT_MS,
7
+ DASHBOARD_DEFAULT_URL,
8
+ DASHBOARD_EPHEMERAL_PORT_HINT,
9
+ DASHBOARD_HELP,
10
+ clampDashboardMs,
11
+ dashboardSiblingUrl,
12
+ } from './lib/dashboard-cli.mjs';
3
13
 
4
- const DEFAULT_INTERVAL_MS = '2000';
5
- const DEFAULT_SNAPSHOT_URL = 'http://127.0.0.1:3000/snapshot';
6
- const DEFAULT_FETCH_TIMEOUT_MS = 5_000;
7
14
  const MAX_FIELD_LEN = 120;
8
15
 
9
- const args = parseArgs({
10
- options: {
11
- interval: {
12
- type: 'string',
13
- short: 'i',
14
- default: DEFAULT_INTERVAL_MS
15
- },
16
- url: {
17
- type: 'string',
18
- short: 'u',
19
- default: DEFAULT_SNAPSHOT_URL
20
- },
21
- timeout: {
22
- type: 'string',
23
- short: 't',
24
- default: String(DEFAULT_FETCH_TIMEOUT_MS)
25
- }
26
- },
27
- allowPositionals: true
28
- });
16
+ function parseDashboardArgs() {
17
+ try {
18
+ return parseArgs({
19
+ options: {
20
+ interval: { type: 'string', short: 'i' },
21
+ url: { type: 'string', short: 'u' },
22
+ timeout: { type: 'string', short: 't' },
23
+ once: { type: 'boolean' },
24
+ help: { type: 'boolean', short: 'h' },
25
+ version: { type: 'boolean', short: 'v' },
26
+ },
27
+ allowPositionals: true,
28
+ });
29
+ } catch (err) {
30
+ const message = err instanceof Error ? err.message.split('. ')[0] : String(err);
31
+ process.stderr.write(`ark-dashboard: ${message}. Run ark-dashboard --help.\n`);
32
+ process.exit(2);
33
+ }
34
+ }
35
+
36
+ const args = parseDashboardArgs();
37
+ if (args.values.help || args.positionals[0] === 'help') {
38
+ console.log(DASHBOARD_HELP);
39
+ process.exit(0);
40
+ }
41
+ if (args.values.version) {
42
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
43
+ console.log(pkg.version);
44
+ process.exit(0);
45
+ }
29
46
 
30
- const parsedInterval = parseInt(args.values.interval, 10);
31
- const interval =
32
- Number.isFinite(parsedInterval) && parsedInterval >= 200 && parsedInterval <= 60_000
33
- ? parsedInterval
34
- : Number(DEFAULT_INTERVAL_MS);
35
- const targetUrl = args.values.url || DEFAULT_SNAPSHOT_URL;
36
- const parsedTimeout = parseInt(args.values.timeout, 10);
37
- const fetchTimeoutMs =
38
- Number.isFinite(parsedTimeout) && parsedTimeout >= 200 && parsedTimeout <= 60_000
39
- ? parsedTimeout
40
- : DEFAULT_FETCH_TIMEOUT_MS;
47
+ const envUrl = process.env.ARK_DASHBOARD_URL?.trim();
48
+ const urlExplicit = Boolean(args.values.url || envUrl);
49
+ const targetUrl = args.values.url || envUrl || DASHBOARD_DEFAULT_URL;
50
+ const interval = clampDashboardMs(args.values.interval, DASHBOARD_DEFAULT_INTERVAL_MS);
51
+ const fetchTimeoutMs = clampDashboardMs(args.values.timeout, DASHBOARD_DEFAULT_TIMEOUT_MS);
52
+ const once = Boolean(args.values.once);
41
53
 
42
54
  const RESET = '\x1b[0m';
43
55
  const RED = '\x1b[31m';
@@ -73,22 +85,6 @@ function sanitizeField(value, maxLen = MAX_FIELD_LEN) {
73
85
  return text;
74
86
  }
75
87
 
76
- function siblingUrl(snapshotUrl, suffix) {
77
- try {
78
- const u = new URL(snapshotUrl);
79
- let basePath = u.pathname;
80
- if (basePath.endsWith('/snapshot')) {
81
- basePath = basePath.slice(0, -'/snapshot'.length);
82
- } else if (basePath.endsWith('/snapshot/')) {
83
- basePath = basePath.slice(0, -'/snapshot/'.length);
84
- }
85
- u.pathname = `${basePath}${suffix}`;
86
- return u.toString();
87
- } catch {
88
- return null;
89
- }
90
- }
91
-
92
88
  /**
93
89
  * @returns {{ ok: true, data: unknown } | { ok: false, kind: 'waiting' | 'failure', error?: string }}
94
90
  */
@@ -340,6 +336,7 @@ function renderHardening(snapshotResult) {
340
336
  } else {
341
337
  const detail = snapshotResult.error ? ` (${snapshotResult.error})` : '';
342
338
  console.log(`${RED}Failure contacting kernel${detail}${RESET}`);
339
+ if (!urlExplicit) console.log(`${MUTED}${DASHBOARD_EPHEMERAL_PORT_HINT}${RESET}`);
343
340
  }
344
341
  return;
345
342
  }
@@ -381,13 +378,13 @@ function renderHardening(snapshotResult) {
381
378
  }
382
379
 
383
380
  async function render() {
384
- process.stdout.write('\x1b[2J\x1b[H');
381
+ if (!once) process.stdout.write('\x1b[2J\x1b[H');
385
382
  console.log(`ArkGate Observability Dashboard`);
386
383
  console.log(`Time: ${sanitizeField(new Date().toISOString())}`);
387
384
  console.log(`Endpoint: ${sanitizeField(targetUrl)}\n`);
388
385
 
389
- const outboxUrl = siblingUrl(targetUrl, '/outbox');
390
- const workflowsUrl = siblingUrl(targetUrl, '/workflows');
386
+ const outboxUrl = dashboardSiblingUrl(targetUrl, '/outbox');
387
+ const workflowsUrl = dashboardSiblingUrl(targetUrl, '/workflows');
391
388
  const [snapshotResult, outboxResult, workflowsResult] = await Promise.all([
392
389
  fetchJsonResult(targetUrl),
393
390
  fetchJsonResult(outboxUrl),
@@ -404,9 +401,15 @@ async function render() {
404
401
  snapshot?.outbox,
405
402
  snapshot?.workflows,
406
403
  );
404
+ return snapshotResult.ok;
407
405
  }
408
406
 
409
407
  async function startDashboard() {
408
+ if (once) {
409
+ const ok = await render();
410
+ process.exitCode = ok ? 0 : 1;
411
+ return;
412
+ }
410
413
  console.log(
411
414
  `Starting ArkGate Observability Dashboard (polling every ${interval}ms, fetch timeout ${fetchTimeoutMs}ms)`,
412
415
  );