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
@@ -74,7 +74,9 @@ Invalid ArkGate config (/repo/ark.config.json):
74
74
  - $.layers[0].forbiddenGlobal: unknown field
75
75
  ```
76
76
 
77
- The same input cannot pass CI while being silently ignored by MCP or ESLint. Invalid JSON, wrong
77
+ The same input cannot pass CI while being silently ignored by MCP or ESLint. In ESLint an
78
+ invalid contract (root config or a referenced ArkRules file) is one `configInvalid` error per
79
+ linted file with the same validator text; it does not crash the lint run. Invalid JSON, wrong
78
80
  types, empty required strings, duplicate string-array entries, negative safety thresholds, and
79
81
  unsupported schema versions also fail before scanning begins.
80
82
 
@@ -132,7 +134,10 @@ Top-level fields:
132
134
  certify a test the project never declared a runner root for.
133
135
  - **`arkRules`** (optional, schema `1.1+`) — map of layer name → project-relative path to an
134
136
  ArkRules file (e.g. `"DomainModel": "arkrules/DomainModel.json"`). Keys must match a declared
135
- layer. Missing/invalid referenced files **fail closed**.
137
+ layer. Missing/invalid referenced files **fail closed**. A `arkrules/*.json` file the map
138
+ does not reference (or any such file when the map is absent) is advisory drift:
139
+ `ARKRULE_FILE_UNREFERENCED` in check `warnings`, `rulesUnderContract.unreferencedFiles` in
140
+ doctor, and `arkRulesCatalog.unreferencedFiles` in MCP `ark_manifest`. It never fails the check.
136
141
  - **`arkRun`** (optional, schema `1.2+`) — inline ArkRun extra (`mode`, `kernelRoots`
137
142
  (`compositionRoots` alias), `managedLayers`, `requireDeclarations`). Absence is silent. Unknown keys fail closed.
138
143
  `managedLayers` must name existing `layers[].name` values. Empty `compositionRoots` in
@@ -142,6 +147,19 @@ Top-level fields:
142
147
  is a policy-delta **weakening**. Enforced extra teeth share the CLI / MCP / hook /
143
148
  preflight / CI verdict and arm only when the layer plane is classified (same ≥50%
144
149
  governed and ≥1 populated-layer floor as ArkRules).
150
+ Kernel factories in a root: `createArkKernel` / `createStrictArkKernel` /
151
+ `createLenientArkKernel`, their `*FromConfig` variants, or `ArkModule.forRoot()` /
152
+ `forRootAsync()` imported from `arkgate/nestjs`. Undeclared-* sensors count only calls
153
+ whose receiver is traced to the kernel (factory-bound local, import from a root module —
154
+ resolved through tsconfig `paths` and barrels that re-export it — a plain alias
155
+ `const kernel = ark`, a `typeof ark` binding, a binding typed `ArkKernel` / `EventBus` /
156
+ publisher from `arkgate/runtime` or `arkgate/nestjs`, its `.eventBus`, a `publisher(..)`
157
+ result), or whose receiver cannot be traced but whose literal name is a kernel-valid
158
+ intent (`Domain.…`, `Application.…`) — `res.send('ok')` or `require.resolve('pkg')` never
159
+ count. Call names are a string literal or a same-file
160
+ `define(..)` / `defineIntent(..)` creator (or string constant); anything else in
161
+ `enforced` mode reports `ARKRUN_INTERACTION_NAME_INCOMPLETE` (partial). See
162
+ [diagnostics](diagnostics.md#ARKRUN_MISSING_ROOT).
145
163
  - **`arkOrder`** (optional, schema `1.3+`) — inline ArkOrder extra (`mode`, `planeRoots`,
146
164
  `managedLayers`, `maxXiKeys`, **`xiKeys`**, optional **`appliesTo`**). Absence is silent.
147
165
  Unknown keys fail closed. Import `createOrderPlane` from `arkgate/order` (same package).
@@ -157,7 +175,11 @@ Top-level fields:
157
175
  at least one glob. Membership ids and recomputable statuses such as `paid` /
158
176
  `overdue` are not keys. Factory options `informationBudget`, `sigmaMaxAgeMs`,
159
177
  `store` (`ReleaseStore`), and capacity packs belong on `createOrderPlane`, not this
160
- extra object. Later ξ is `proposeRelease` then `apply`; `refreshSigma`; ingest
178
+ extra object. Literal evidence of either still reports statically: a ttl/freshUntil/maxAge
179
+ key in a plane `release` / `proposeRelease` ξ literal (`ARKORDER_XI_TTL`), and a literal
180
+ `allowedKinds` entry denied by a literal `cannotObserve` (`ARKORDER_INFORMATION_BUDGET`).
181
+ Later ξ is `proposeRelease` then `apply` (a stale proposal fails
182
+ `ARKORDER_STALE_PROPOSAL`); `refreshSigma`; ingest
161
183
  residual `absorb | escalate_up | hold`. Demotion or deletion is a policy-delta
162
184
  **weakening**. Field ingest never mints a pattern.
163
185
 
@@ -230,6 +252,18 @@ That sentence is product copy. Not “Rich domain model, business rules, and dom
230
252
  ```
231
253
 
232
254
  - `intentPrefixes`, `forbiddenGlobals`, `mayImportInfrastructure`, `optional`
255
+ — `intentPrefixes` is how the ArkRun kernel maps an intent name to a layer at runtime.
256
+ A layer with a canonical 11-layer name (`DomainModel`, `ApplicationOrchestration`,
257
+ `PersistenceAdapters`, … — what `ark init` writes) and no `intentPrefixes` gets the
258
+ built-in prefixes (`Domain.`, `Application.`, `Adapter.Persistence.`, …), so default configs
259
+ enforce hard observed-layer-flow as-is. A custom-named layer in an `allowed: false` rule
260
+ with no `intentPrefixes` cannot map any intent: `createStrictArkKernelFromConfig` /
261
+ `createArkKernelFromConfig` still build (strict default) and record one
262
+ `layer.observedFlowUnresolvable` audit record (`ARKRUN_LAYER_FLOW_UNRESOLVABLE`), and
263
+ `ark doctor` lists those layers when the `arkRun` extra is on. Passing
264
+ `enforceObservedLayerFlow: 'hard'` explicitly throws `ArkKernelConfigError` instead.
265
+ `peerIsolation` rules are file-path slice walls; runtime flow (producer/intent names only)
266
+ does not evaluate them.
233
267
  - `reserved` / `allowEmpty` — future houses whose globs match nothing yet. `--strict-config` does not fail; `CONFIG_LAYER_PATTERN_NO_MATCHES` (typo warning) is skipped. A typo warning fires only when the glob is not reserved.
234
268
  - `capabilities: { deny: [...] }` — opt-in effect walls over the seven capability ids
235
269
  (`network`, `filesystem`, `clock`, `randomness`, `environment`, `process`, `persistence`);
@@ -251,7 +285,7 @@ process module-capability family must be denied.
251
285
  Rule fields:
252
286
 
253
287
  - `from`, `to`, `allowed`, `message`, `peerIsolation`, `sliceFolders`, `sliceIdentity`,
254
- `sharedRoots`, `allowedCrossSlice`, `sharedImportsSlice`
288
+ `sharedRoots`, `allowedCrossSlice`, `sharedImportsSlice`, `childSlices`
255
289
  - `peerIsolation: true` + `allowed: false`: deny only when slice ids differ; same-slice allows
256
290
  when both paths classify. Applies to **any** declared `from`→`to` pair, not only self-edges.
257
291
  Missing paths, empty slice folders, or unclassifiable slices **fail closed** (deny — cannot
@@ -269,13 +303,162 @@ Rule fields:
269
303
  off that prefix (`src/app/api/projects/...`) is not that slice.
270
304
  - `sliceIdentity` is `path` or `stars`. Absent and `path` are the same: today's ids,
271
305
  byte for byte, so baselines and `allowedCrossSlice` keep working with no migration.
272
- `stars` names a starred prefix as the last literal plus the star bindings.
306
+ `stars` names a starred prefix as the last literal plus every star binding. It
307
+ drops only the literals before the last literal; a star before it is kept, so
308
+ `modules/*/api/*` binds `orders/api/v1` and `users/api/v1`, never one shared `api/v1`.
309
+ arkgate 4.8.23 dropped that star too (`api/v1`). For one release a `sliceAliases`
310
+ `to`, an `allowedCrossSlice` entry, or a `childSlices.allowedCrossSlice` entry written
311
+ against the 4.8.23 id still loads and keeps its 4.8.23 verdict: a legacy alias target
312
+ (`api/v1/x`) joins the universe whose 4.8.23 id it names on each edge, and a legacy
313
+ allowance still clears the edges it cleared. ark-check warns `CONFIG_SLICE_LEGACY_STARS_ID`
314
+ (`failsStrict: false`) and names the new id to write (`orders/api/v1/x`, or one entry
315
+ per module when several modules share that old id). A legacy alias target is rejected at
316
+ config load only when it maps to more than one universe shape of the rule
317
+ (`maps to more than one universe shape`). Rewrite these entries before the next minor
318
+ release, which drops the legacy form.
273
319
  `lib/features/*/*` and `lib/repositories/features/*/*` both yield
274
320
  `features/projects/rfi`, so one feature has one id across parallel trees. Bare names
275
321
  (`features`) are unchanged. Doctor warns when two different prefixes bind as the
276
322
  same id, and the warning names both paths (`admin/features/*` and `public/features/*`
277
323
  both bind as `features/*`). That warning is advisory. Unrelated trees that share a
278
324
  last folder name should stay on `path`.
325
+ - `childSlices` is an optional inner wall on a `peerIsolation` rule. It runs only with
326
+ `peerIsolation: true` and `allowed: false` on the same rule. Elsewhere it is inert: config
327
+ load still accepts it (4.8.23 did), and ark-check warns `CONFIG_CHILD_SLICES_INERT`
328
+ (`failsStrict: false`) with the fix — add both flags to the rule, or remove `childSlices`.
329
+ Absent means today's
330
+ universe wall, byte for byte: no `reasonId`, same messages, same doctor output.
331
+ `sliceFolders` names the children. `sliceIdentity` is the same `path` | `stars` choice, and
332
+ the child id must extend the universe id. With `"sliceIdentity": "stars"`, `lib/features/*/*`
333
+ yields child ids like `features/projects/rfi` under the universe id `features/projects`. Under
334
+ the default `path` identity the child id keeps the full prefix (`lib/features/projects/rfi`),
335
+ which does not extend a bare-name universe id such as `features/projects`: the check warns
336
+ `CONFIG_CHILD_SLICE_EXTENDS` and the child wall stays off. Pair a bare-name universe with
337
+ `"sliceIdentity": "stars"` on `childSlices`. A flat file whose child id does not grow
338
+ past the universe id is universe common. So is a `commonFolders` directory (a name such as
339
+ `domain`) **directly under the universe**: `lib/features/projects/domain/**`. A folder of
340
+ that name inside a child (`lib/features/projects/rfi/domain/**`) belongs to that child, so a
341
+ sibling that imports it is `CROSS_SIBLING_SLICE` and the child's own `domain/` may import
342
+ its own feature. A child may import universe common code. Common code may import a child only when
343
+ `parentMayImportChild` is true (default false). `siblings` is `deny` (default, also when the
344
+ key is omitted), `advisory`, or `{ "default": "deny" | "advisory", "enforce": [...] }`.
345
+ The universe wall runs first. A denied cross-universe edge is
346
+ `reasonId` `CROSS_PARENT_SLICE` and is never advisory. A denied sibling edge is
347
+ `CROSS_SIBLING_SLICE`: an error when `siblings` is `deny` or `{ "default": "deny" }`,
348
+ a warning (`failsStrict: false`) when `advisory`. With `{ "default": "advisory", "enforce" }`,
349
+ a crossing whose importer is listed is an error. Every other sibling crossing stays a warning.
350
+ `enforce` entries are child slice ids (`features/projects/rfi`) or subtree paths
351
+ (`lib/features/projects/rfi` or `src/lib/features/projects/rfi`). A child id matches that
352
+ importer's child id. A path matches when the importer file sits in that directory.
353
+ Bare names do not match. `*` is not a wildcard on this list. `default: "deny"` denies every
354
+ sibling crossing; the list cannot loosen it. Advisory is advisory on day 1: with
355
+ `--baseline` (or an `--against` base), the anti-growth ratchet is judged **per rule** and
356
+ only over advisory crossings. `siblings.ratchet` on the object form picks it:
357
+ absent — on only when the baseline in use already records at least one advisory
358
+ crossing of that rule, and then only a crossing past that recorded count fails (an empty
359
+ baseline, or one frozen before the child wall, promotes nothing). The recorded count is
360
+ what the baseline holds for that rule: a key whose crossing was removed still counts when
361
+ the rule still classifies that edge as an advisory sibling crossing, so swapping one
362
+ recorded crossing for a new one keeps the count and stays a warning; `true` — any unrecorded
363
+ advisory crossing fails whenever a baseline is in use (the durable lock); `false` — measure
364
+ only, never fails (doctor `slices.crossSibling` still counts). A recorded enforced crossing
365
+ never switches the advisory ratchet on. A promoted finding says why and names
366
+ `ratchet: false`. `enforce` importers and `default: "deny"` stay errors either way. Doctor
367
+ `productHonesty` applies the same ratchet as `ark-check --baseline`. `ratchet: false` is a
368
+ weakening policy delta and `ratchet: true` a strengthening one. arkgate 4.8.23 and older
369
+ reject `ratchet` (`unknown field` at `$.rules[n].childSlices.siblings.ratchet`).
370
+ `childSlices.message` is the text for inner-wall findings (`CROSS_SIBLING_SLICE` and
371
+ universe common importing a child). The rule `message` stays the universe-wall text
372
+ (`CROSS_PARENT_SLICE` and the fail-closed denies). Without `childSlices.message`, an
373
+ inner-wall finding gets ArkGate's own default text; it never reuses the rule message.
374
+ Every surface uses the same choice: ark-check / CI, the write hook, MCP `validate_code`,
375
+ ESLint, and `analyzeProject` / prepare-change. Editing it is not a policy delta. arkgate
376
+ 4.8.23 and older reject it (`unknown field` at `$.rules[n].childSlices.message`). The baseline key stays `ruleId|file|fromLayer|toLayer|target` and does
377
+ not include `reasonId`. `productHonesty.finished` stays false while any child wall is
378
+ advisory, including `{ "default": "advisory", "enforce": [...] }`. The list does not finish
379
+ the house. Doctor `slices` is `{ crossParent, crossSibling, pairs }` for the directed universe
380
+ pairs that occur. A child id that does not extend its universe id warns
381
+ `CONFIG_CHILD_SLICE_EXTENDS` and is treated as universe common. arkgate 4.8.22 and older
382
+ reject `childSlices`. Ark warns `CONFIG_CHILD_SLICES_VERSION` only when the repo pins such
383
+ an older arkgate: an exact `package.json` dependency, the installed `node_modules/arkgate`,
384
+ `package-lock.json`, a `scripts/*hook*` / `.husky` / host hook command, or a CI workflow
385
+ (`pedroknigge/arkgate@vX.Y.Z` or its `version:` input). The finding points at that file and
386
+ line and says which version to bump to. Without such evidence it is silent. Ranges
387
+ (`^4.8.0`) are not evidence; pnpm/yarn lockfiles are read through the installed copy. The
388
+ same check covers `sliceIdentity` (4.8.21), `sharedImportsSlice` (4.8.20),
389
+ `"deny-cross-parent"` (4.8.23), and the keys added after 4.8.23 (`stopAt`,
390
+ `childSlices.message`, `siblings.ratchet`). A build that still types `siblings` as
391
+ the string enum `deny | advisory` rejects the object at config load
392
+ (`must be one of deny, advisory` at `$.rules[n].childSlices.siblings`). It does not ignore
393
+ the object. No published release through 4.8.22 accepts `{ default, enforce }`. Ship that
394
+ form only on a release that includes it.
395
+ `childSlices.allowedCrossSlice` is a directed list on the child block only:
396
+ `{ "from": "features/projects/*", "to": "features/projects/d2d-item" }`.
397
+ `*` matches one whole path segment. `**` and a partial segment (`feat*`) are
398
+ rejected at config load. A bare name (no `/`) is rejected, because it is
399
+ ambiguous across universes. The list clears only a `CROSS_SIBLING_SLICE`,
400
+ and only after the universe wall has allowed the edge. It never clears
401
+ `CROSS_PARENT_SLICE` or `CROSS_PARENT_VIA_SHARED`. A pattern that can name
402
+ two universes (`features/*/*` to `features/management/eos`) leaves the
403
+ cross-parent finding in place and warns `CONFIG_CHILD_SLICE_CROSS_UNIVERSE`
404
+ (`failsStrict: false`). The universe `allowedCrossSlice` does not read this
405
+ list and still treats `*` as a literal slice id, byte for byte with 4.8.22.
406
+ Adding an entry is a weakening policy delta. Removing one is strengthening.
407
+ Additions and removals together need a judgment. A wildcard that widens a
408
+ literal entry is weakening and needs the same acknowledgement. A build that
409
+ accepts `childSlices` and still rejects unknown fields fails at config load
410
+ (`unknown field` at `$.rules[n].childSlices.allowedCrossSlice`). It does not
411
+ ignore the key. No published release through 4.8.22 accepts the wildcard.
412
+ Ship it only on a release that includes this field.
413
+ `childSlices.sliceAliases` maps files that sit outside every slice folder onto
414
+ a child that already exists: `{ "from": "lib/compliance/**", "to": "features/projects/compliance" }`.
415
+ A `from` without a wildcard in its last segment is a folder form and also covers
416
+ everything under it, as `sharedRoots` does (`lib/compliance` equals `lib/compliance/**`).
417
+ `to` must match a universe `sliceFolders` shape of the rule, plus one child segment.
418
+ A bare name, a universe id alone, a target outside every universe shape, and a
419
+ wildcard in `to` are rejected at config load (`child of a universe shape`); under
420
+ `"sliceIdentity": "stars"` a target written against the 4.8.23 id loads with a
421
+ `CONFIG_SLICE_LEGACY_STARS_ID` warning (see `sliceIdentity` above). Config
422
+ load cannot see the tree, so it checks only the shape: doctor marks an alias move
423
+ whose target universe no scanned file belongs to (`unknownUniverse`, "check for a
424
+ typo"). The source glob may cover only files the slice folders do not already classify. Overlap with a
425
+ universe `sliceFolders` entry or a child `sliceFolders` entry is rejected
426
+ (`overlaps a slice folder`). Two aliases that can match one file are rejected
427
+ (`two slice aliases match the same file`). An aliased file takes the target
428
+ universe id and the target child id for both walls, so an import into another
429
+ universe is `CROSS_PARENT_SLICE` and an import into another child of the same
430
+ universe is `CROSS_SIBLING_SLICE`. Same-child imports and imports into that
431
+ universe's common folders follow the existing child wall. Doctor lists every
432
+ alias as an owed move: the source glob, the target slice, the destination
433
+ folder, and the files. The section says the files are not finished. Aliases
434
+ are debt. `productHonesty.finished` stays false while any alias is set.
435
+ Adding aliases is a strengthening policy delta (unclassified files come under
436
+ the walls). Removing them is weakening (those files leave the walls) and needs
437
+ the same hash-bound acknowledgement as any other weakening. Additions and
438
+ removals in one change, including a retarget, are judgment-required. A folder
439
+ edit already asks for a judgment and covers the alias list. The baseline key
440
+ stays `ruleId|file|fromLayer|toLayer|target` and does not include `reasonId`.
441
+ The write hook, ESLint, snippet analysis, ark-check, and CI all see the alias
442
+ through one resolver. There is no skip flag. arkgate 4.8.22 and older reject
443
+ `childSlices`. A build that accepts `childSlices` and still rejects unknown
444
+ fields fails at config load (`unknown field` at `$.rules[n].childSlices.sliceAliases`).
445
+ It does not ignore the key. No published release through 4.8.22 accepts
446
+ `sliceAliases`. Ship a config that sets it only on a release that includes
447
+ this field. The write hook, ESLint, snippet analysis, ark-check, and CI all
448
+ see the per-edge decision. In ESLint an error-level crossing reports on
449
+ `ark/no-domain-infra-imports` and an advisory (warning) crossing reports on the
450
+ warn-level `ark/architecture-advisory`, because ESLint severity is per rule id
451
+ ([ai-gates — ESLint](ai-gates.md#eslint-editor-feedback--bounded-parity-envelope)).
452
+
453
+ Doctor may suggest moving a flat file that sits at universe level when
454
+ exactly one child slice imports it. The card names the file, that child, and
455
+ a destination folder inside the child that keeps the file's layer. It is a
456
+ suggestion. It is not a config field, not a finding, and not an ark-check
457
+ result. The importers come from the same resolved graph ark-check uses (tsconfig
458
+ path aliases included). A file two children import stays where it is, and so
459
+ does a file that anything outside that one child imports (universe common, a
460
+ shared root, another universe, another layer). A move that the walls would deny
461
+ for any recorded importer is never suggested.
279
462
 
280
463
  ```jsonc
281
464
  {
@@ -346,6 +529,37 @@ is evidence:
346
529
  to `"deny"`. That hop is direct only — `allowedCrossSlice` does not excuse it. A green
347
530
  `ark-check` prints one line per layer edge (the count and three examples) and says the
348
531
  wall is direct-only. Every edge stays in `ark-check --json` and `ark-check --doctor`.
532
+ `"deny-cross-parent"` does not change that direct hop: it stays a `SHARED_IMPORTS_SLICE`
533
+ warning. It adds a whole-graph finding when a slice reaches a **different** universe
534
+ only through a shared root (`reasonId` `CROSS_PARENT_VIA_SHARED` on
535
+ `LAYER_IMPORT_VIOLATION`). A shared hop that stays inside one universe is not a finding.
536
+ That pass runs in `ark-check` and CI. The write hook and ESLint see one edge at a time
537
+ and do not block it. arkgate 4.8.22 and older reject the value at config load (`must be
538
+ one of deny`). They do not ignore it and they do not treat it as `"deny"`.
539
+ A universe edge the importer's rule declares in `allowedCrossSlice` (directed) is not
540
+ reported through a shared root either: it is the same dependency the direct wall
541
+ already allows. `childSlices.allowedCrossSlice` still never clears it.
542
+ In a DI app the composition root (bootstrap, registrations) has to sit under
543
+ `sharedRoots`, and every slice that asks the root for a service then "reaches" every
544
+ universe the root registers. Name those files in the object form:
545
+ `"sharedImportsSlice": { "mode": "deny-cross-parent", "stopAt": ["kernel/bootstrap.ts",
546
+ "kernel/registrations/**", "lib/feature-registrations.ts"] }`. The walk never starts at
547
+ or passes through a stop file, so a path through it is not a crossing. A `stopAt` entry
548
+ matches like a shared root (anchored, optional leading `src/` or `app/`, a plain folder
549
+ covers its subtree) or like an alias glob (`kernel/registrations/**` against
550
+ `src/kernel/registrations/x.ts`). Config load rejects an entry that covers the whole
551
+ tree (`*`, `**`, `src`, `src/**`, `app/**`, `*/**`) or a whole layer root of that rule
552
+ (layer `src/lib/**` with `lib/**`), since it would stop every shared node and silence
553
+ the walk: `must not cover the whole tree` / `must not cover a whole layer root of this
554
+ rule`. A stop file must still
555
+ sit under `sharedRoots`; `stopAt` does not classify files and does not silence the direct
556
+ `SHARED_IMPORTS_SLICE` warning. A slice destination that matches a stop is still reported.
557
+ Each finding carries `via` (the shared hops). Doctor `sharedWalkHubs` names a shared file
558
+ that sits on at least half of ten or more such findings — a likely composition root. That
559
+ is advisory, not a finding. Adding stops is a weakening policy delta
560
+ (`shared-walk-stop-added`), removing them strengthens, both together need a judgment. The
561
+ string forms are unchanged. arkgate 4.8.23 and older reject the object at config load
562
+ (`must be one of deny, deny-cross-parent`).
349
563
  - `allowedCrossSlice` entries match a full slice id (`features/catalog`) or a bare slice name
350
564
  (`catalog`), and only in the direction written. The reverse edge still denies. A bare name
351
565
  matches that name under **any** slice folder, so in a repo with several slice parents
@@ -357,7 +571,10 @@ is evidence:
357
571
  about your code) versus `unclassifiable path (src/widgets/x.tsx)` (a fact about our evidence).
358
572
  `no slice folders` and `no path evidence` are the two remaining evidence reasons. A rule-level
359
573
  `message` override no longer hides it: the reason is appended to your text, not replaced by it.
360
- - Both declarations are **weakening** changes in `ark policy-delta`
574
+ The write hook and MCP `validate_code` say the same, and for an evidence reason their fix
575
+ asks you to move the file into a slice or declare its root in `sharedRoots`.
576
+ - Both declarations are **weakening** changes in the policy delta (`arkgate-check --strict-merge`
577
+ with `--policy-base <file>` / `--policy-base-ref <ref>`, or MCP `ark_policy_delta`)
361
578
  (`shared-roots-added`, `cross-slice-allowance-added`), so a policy review sees them. Both are
362
579
  inert on a rule without `peerIsolation: true`, and policy-delta stays silent about them until
363
580
  the wall exists.
@@ -373,7 +590,8 @@ other choice deliberately, not so slices can drift into a mesh.
373
590
  runtime coupling. They still appear on the **violations** list with `typeOnly: true`,
374
591
  `failsStrict: false`, and adapter diagnostic **severity: warning** so doctor/HTML keep
375
592
  `violations.typeOnly` / `typeEdgePolicy` honest — but they **do not** fail merge/exit, library
376
- `valid`, or preflight the way **value** edges do. **Exception:** `peerIsolation` slice
593
+ `valid`, or preflight the way **value** edges do. ESLint reports them as warnings on
594
+ `ark/architecture-advisory`, not as errors. **Exception:** `peerIsolation` slice
377
595
  boundaries stay hard even for type-only. A value import of a pure-type barrel is still a value
378
596
  edge (not soft-skipped). Prefer placing shared types in a **SharedTypes** (or owning) layer
379
597
  both sides may import. Optional starter: [`templates/layers/shared-types.starter.json`](../templates/layers/shared-types.starter.json)
@@ -435,8 +653,8 @@ Each `arkrules/<Layer>.json` may declare:
435
653
 
436
654
  | Section | Purpose | Modes | What it really enforces |
437
655
  |---------|---------|--------|-------------------------|
438
- | `structure[]` | Closed sensor ids (e.g. `orchestration-only`, `thin-adapter`, `writes-via-aggregate`, `aggregate-private-state`, `always-valid-factory`, `domain-event-on-mutation`, `no-anemic-model`) | `advisory` (default) or `enforced` | **Heuristics of module shape** — not proof that logic was extracted to Domain. `writes-via-aggregate` is driver-import + write-token in the declaring layer (ADR 0032). Tier-2 sensors (`no-anemic-model`) stay advisory-only (cannot promote to enforced). |
439
- | `invariants[]` | Stable ids + description + `coverage` (`test` / `symbol`) + optional `appliesTo` globs | `advisory` or `enforced` | **Named policy + evidence.** `coverage.symbol` means a declaration of that identifier in a non-test file (witness path `symbolEvidenceFile`; additive `shape`: `function`, `class`, `const`, `method`, `type`, `interface`, or `enum`). Imports and test calls do not count. A `describe` / `it` / `test` / `context` title may also name the invariant id. A comment, string, or other bare mention does not. Does **not** execute tests or business logic and does **not** replace behavior/property tests. Doctor and `--rules-inventory` print the witness path and discard counts on green runs too. A declaration match is not “the tests pass.” |
656
+ | `structure[]` | Closed sensor ids (e.g. `orchestration-only`, `thin-adapter`, `writes-via-aggregate`, `aggregate-private-state`, `always-valid-factory`, `domain-event-on-mutation`, `no-anemic-model`) | `advisory` (default) or `enforced` | **Heuristics of module shape** — not proof that logic was extracted to Domain. `writes-via-aggregate` is driver-import + write-token in the declaring layer (ADR 0032). Tier-2 sensors (`no-anemic-model`) stay advisory-only (cannot promote to enforced). Class shapes come from a tokenizer that skips strings, comments, template literals and regex literals; a class it cannot walk to its end is reported as `shape analysed until character N` (an error under an enforced sensor), never passed in silence. |
657
+ | `invariants[]` | Stable ids + description + `coverage` (`test` / `symbol`) + optional `appliesTo` globs | `advisory` or `enforced` | **Named policy + evidence.** `coverage.symbol` means a declaration of that identifier in a non-test file (witness path `symbolEvidenceFile`, project-relative; additive `shape`: `function`, `class`, `const`, `method`, `type`, `interface`, or `enum`). A `Class.member` symbol (e.g. `Order.ensureInvariants`) requires a member with that name inside the body of `class Class`, a `const Class = class { … }` expression, a `namespace Class { … }` (function or const), or a top-level key of a `const Class = { … }` object literal; a member of another container, a nested object key, a free function, or a file that only mentions the class name does not count. The search is not restricted to the invariant's layer. `coverage.test: false` without a `coverage.symbol` declares no evidence: it is advisory-only (an enforced invariant with that shape reports `INVARIANT_UNCOVERED` and fails strict; the rest of the catalog still evaluates) and cannot be promoted. Imports and test calls do not count. A `describe` / `it` / `test` / `context` title may also name the invariant id. A comment, string, or other bare mention does not. Does **not** execute tests or business logic and does **not** replace behavior/property tests. Doctor and `--rules-inventory` print the witness path and discard counts on green runs too. A declaration match is not “the tests pass.” |
440
658
 
441
659
  **Reporting:** diagnostics carry `evidence.arkruleId` + `evidence.arkruleSource`. Label residual
442
660
  **`[Layer]`** vs **`[ArkRules]`** in agent output. Doctor / HTML: `rulesUnderContract` (catalog +
@@ -480,6 +698,14 @@ npx ark-check --strict-config --policy-base ./before.ark.config.json --json
480
698
  npx ark-check --strict-merge --policy-base-ref origin/main
481
699
  ```
482
700
 
701
+ ArkRules are part of the policy. With a Git ref, the base `arkrules/*.json` files are read from
702
+ that same ref (a file the base config references but the ref does not contain yet counts as an
703
+ empty layer). With `--policy-base <file>`, the base ArkRules paths resolve next to that file (keep
704
+ a copy of the base `arkrules/` beside it; the error names that directory and the
705
+ `--policy-base-ref` alternative). A base ArkRules file that cannot be read or parsed fails
706
+ closed, because comparing the candidate catalog with itself would read every demotion or
707
+ deletion as neutral.
708
+
483
709
  The additive JSON result includes `policyDelta`: both policy hashes, the overall classification,
484
710
  stable findings, and `blockingFindingIds`. Supported comparisons cover governed include/exclude
485
711
  roots, layer patterns/exclusions/forbidden globals, deny rules, same-layer peer isolation,
@@ -536,14 +762,40 @@ type than product source:
536
762
 
537
763
  | Check | What it does |
538
764
  |-------|----------------|
539
- | `ark-check --local --base origin/dev` | Opt-in local / multi-worktree cheap check. Same engine as `--changed`. Refused with `--strict-merge`. `ARK_CHECK_LOCAL=1` is the same unless CI or a full-tree report mode is on. |
765
+ | `ark-check --local --base origin/dev` | Opt-in local / multi-worktree cheap check. Same engine as `--changed`. Refused with `--strict-merge` and with `--update-baseline`. `ARK_CHECK_LOCAL=1` is the same unless CI, a full-tree report mode, or `--update-baseline` is on. |
540
766
  | `ark-check --changed --base origin/dev` | Layer check on touched sources only. A CSS/i18n PR pays almost nothing. |
541
767
  | `ark-check --against origin/dev` | New violation keys vs **that ref's** baseline (not only HEAD). |
542
768
  | `ark-check --contract-diff --base origin/dev` | Classifies tighten / loosen / reclassify / baseline-grow. |
543
- | `--contract-session --author <id>` | Law-only PR. Mixed law+product still fails. Loosen/grow need a session even with an empty `stewards[]`; a non-empty list also needs a matching listed author. |
769
+ | `--contract-session --author <id>` | Law-only PR. Mixed law+product still fails. Loosen/grow need a session even with an empty `stewards[]`; a non-empty list also needs a matching listed author. A session always classifies the law change against the team base (`--base`, or the discovered `main`/`dev`), including under `--strict-merge`. If that base has no contract yet (the adoption PR that adds `ark.config.json`), there is nothing to loosen and the session proceeds; an explicit `--policy-base-ref` without a contract at that ref still exits 2. |
544
770
  | `--persona touch\|contributor\|agent\|steward` | Budget presets for the same teeth. |
545
771
  | `ark status --vs origin/dev` | One line: pin / contract / baseline drift vs that ref. |
546
772
 
773
+ How the changed-file set is computed (`--changed`, `--local`, `--against`, `--persona`):
774
+
775
+ - Paths are relative to `--root`, even when that root is a package inside a monorepo
776
+ (`git diff --relative`; base-ref files are read as `<ref>:./<path>`). Changes outside the
777
+ root are not part of that package's diff.
778
+ - Committed changes since the merge base, staged, unstaged, and untracked files all count.
779
+ File names with spaces, quotes, or non-ASCII characters are kept exactly.
780
+ - Without `--base` / `--against`, the base is the first of `origin/dev`, `origin/main`,
781
+ `origin/master`, `dev`, `main`, `master` that exists. When none exists (or the root is
782
+ not a git repository), `--changed` and a non-steward `--persona` stop with **exit 2**
783
+ (`reasonId: changed-needs-base`; `--local` uses `local-needs-base`). `--contract-diff`
784
+ and `--persona steward` (which implies it) also stop with **exit 2**
785
+ (`reasonId: contract-diff-needs-base`): the contract and baseline diff has nothing to
786
+ compare against. Pass `--base <ref>` or run a plain full check. A diff that could not be
787
+ computed never counts as "no changes".
788
+ - Any failed git listing (bad ref, timeout, output too large) is exit 2
789
+ (`reasonId: changed-paths-unavailable`), never a pass.
790
+ - `--report` applies the committed `.ark-baseline.json` implicitly, except under
791
+ `--strict` / `--strict-merge` / `--contract-diff` / `--against`: a merge verdict uses a
792
+ baseline only when `--baseline` is passed.
793
+ - `--update-baseline` always freezes the whole tree: it refuses `--changed` / `--local`
794
+ (exit 2) and ignores `ARK_CHECK_LOCAL=1`, so a partial scan can never truncate
795
+ `.ark-baseline.json`.
796
+ - Layer-pattern typo warnings (`CONFIG_LAYER_PATTERN_NO_MATCHES`) under `--changed` are
797
+ judged against the full governed tree, so an untouched layer is not reported as a typo.
798
+
547
799
  Write-gate ApplyPatch denies a batch that mixes law files with product source. Humans who
548
800
  never hit PreToolUse are unchanged. Local `pnpm` gates should call `--local --base` or
549
801
  `--changed --base`, not only full-tree `--strict-merge`. Write hooks stay on the lexical
@@ -554,11 +806,20 @@ worktree has its own root); there is no machine-wide analysis lock.
554
806
  include tree. File-local ArkRules sensors (class shape, orchestration-only, thin-adapter,
555
807
  writes-via-aggregate) and hint preload stay on the touched set. Layer and cycle sensors
556
808
  see that closure, so a new illegal import or a cycle the change can complete still
557
- fails. Untouched files outside the closure are left to the full-tree CI check. Same
809
+ fails. When a touched file sits under the `sharedRoots` of a `"deny-cross-parent"` rule,
810
+ the graph runs full-tree: the finding belongs to the slice that imports the shared root,
811
+ which may be outside the closure. A `CROSS_PARENT_VIA_SHARED` path counts as part of the
812
+ change when any file on it changed. Untouched files outside the closure are left to the full-tree CI check. Same
558
813
  engine; not a second analysis path. A `--changed` pass is not a full-tree structural
559
814
  verdict.
560
815
 
561
816
  MCP clients can call `ark_policy_delta` with the previous `baseConfig`, an optional candidate
562
817
  contract (the current project contract is the default), and the same optional acknowledgement.
818
+ When `baseConfig` maps `arkRules`, pass the base catalog as data in `baseArkRuleFiles`
819
+ (`{ "<path from baseConfig.arkRules>": <ArkRules file JSON> }`); without it the call is refused
820
+ instead of returning a config-only verdict. The candidate catalog is read from disk when the
821
+ candidate is omitted or maps the same `arkRules` files as the project contract (after the loader's
822
+ normalisation, so the project `ark.config.json` passed verbatim counts); otherwise pass
823
+ `candidateArkRuleFiles` in the same shape.
563
824
  It invokes the public classifier directly, is read-only, and marks a blocking result as an MCP
564
825
  error without maintaining separate adapter policy.
package/docs/develop.md CHANGED
@@ -214,7 +214,8 @@ stays plane-free. Empty `planeRoots` in `enforced` mode fails closed
214
214
  (`ARKORDER_MISSING_PLANE`). In-memory. Not durable. Does not replace ArkRun.
215
215
 
216
216
  First freeze is `release()`. Later pattern change is `proposeRelease` then
217
- `apply` (`ARKORDER_UNVALVED_RELEASE`). `refreshSigma`; ingest residual
217
+ `apply` (`ARKORDER_UNVALVED_RELEASE`); `apply` only takes a proposal computed against the
218
+ current Release (`ARKORDER_STALE_PROPOSAL`). `refreshSigma`; ingest residual
218
219
  `absorb | escalate_up | hold` + `reasonCode`; capacity pack as data;
219
220
  `createMemoryReleaseStore`; `ingestTravelAction`; ArkRun `decisionTape`.
220
221