arkgate 4.8.23 → 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.
- package/CHANGELOG.md +68 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +24 -77
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +596 -112
- package/bin/ark-mcp-runtime.mjs +412 -138
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +39 -0
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/agent-projection-command.mjs +5 -1
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +50 -6
- package/bin/lib/ark-order-doctor.mjs +1 -1
- package/bin/lib/ark-order-facts.mjs +410 -15
- package/bin/lib/ark-order-invariants.mjs +51 -9
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +2 -0
- package/bin/lib/ark-run-doctor.mjs +14 -0
- package/bin/lib/ark-run-facts.mjs +434 -11
- package/bin/lib/ark-run-sensors.mjs +5 -3
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrules-sensors.mjs +50 -249
- package/bin/lib/check-args.mjs +23 -1
- package/bin/lib/ci-and-commands.mjs +16 -6
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/codex-home.mjs +4 -2
- package/bin/lib/config-contract-slices.mjs +701 -0
- package/bin/lib/config-contract.mjs +5 -493
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +16 -9
- package/bin/lib/doctor-advisories.mjs +25 -1
- package/bin/lib/doctor-next-actions.mjs +17 -2
- package/bin/lib/doctor-plan.mjs +6 -8
- package/bin/lib/effective-contract-load.mjs +121 -59
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/first-run-help.mjs +3 -2
- package/bin/lib/flat-parent-importers.mjs +137 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +29 -2
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +1 -1
- package/bin/lib/hook-templates.mjs +18 -16
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage.mjs +59 -11
- package/bin/lib/mcp-adoption.mjs +9 -1
- package/bin/lib/mcp-hook-payload.mjs +401 -61
- package/bin/lib/package-manager.mjs +59 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +81 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +168 -10
- package/bin/lib/prepare-change.mjs +120 -6
- package/bin/lib/prepare-write.mjs +1 -0
- package/bin/lib/presets.mjs +3 -5
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +315 -55
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +179 -5
- package/bin/lib/scan-files.mjs +32 -0
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/stable-hash.mjs +90 -0
- package/bin/lib/start-install-recovery.mjs +71 -1
- package/bin/lib/start-preview.mjs +30 -0
- package/bin/lib/status-command.mjs +33 -4
- package/bin/lib/status-manifest.mjs +32 -4
- package/bin/lib/team-parliament-io.mjs +77 -75
- package/bin/lib/team-parliament.mjs +6 -1
- package/bin/lib/write-path-capabilities.mjs +4 -2
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-MBXCFLO4.js +16 -0
- package/dist/chunk-RNQHRDCQ.cjs +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-BJbaeoj4.d.cts} +18 -3
- package/dist/configTypes-BJbaeoj4.d.ts +246 -0
- package/dist/eslint/index.cjs +17 -4
- package/dist/eslint/index.d.cts +359 -0
- package/dist/eslint/index.d.ts +130 -156
- package/dist/eslint/index.js +10 -4
- package/dist/index.cjs +35 -34
- package/dist/index.d.cts +5475 -0
- package/dist/index.d.ts +3312 -299
- package/dist/index.js +35 -34
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +804 -290
- package/dist/runtime/index.d.ts +2678 -108
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-DRinG-vT.d.cts} +576 -140
- package/dist/types-DRinG-vT.d.ts +1792 -0
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +84 -15
- package/docs/ai-gates.md +160 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +196 -26
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +96 -19
- package/docs/package-surface.md +31 -8
- package/docs/product-voice.md +1 -1
- package/package.json +59 -18
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +47 -10
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +4 -1
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-runtime.md +6 -1
- package/dist/types-Ik_OakcI.d.ts +0 -183
package/docs/configuration.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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`);
|
|
@@ -269,19 +303,43 @@ 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
|
|
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`.
|
|
279
|
-
- `childSlices` is an optional inner wall on a `peerIsolation` rule.
|
|
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
|
|
280
330
|
universe wall, byte for byte: no `reasonId`, same messages, same doctor output.
|
|
281
|
-
`sliceFolders` names the children
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
285
343
|
`parentMayImportChild` is true (default false). `siblings` is `deny` (default, also when the
|
|
286
344
|
key is omitted), `advisory`, or `{ "default": "deny" | "advisory", "enforce": [...] }`.
|
|
287
345
|
The universe wall runs first. A denied cross-universe edge is
|
|
@@ -293,15 +351,43 @@ Rule fields:
|
|
|
293
351
|
(`lib/features/projects/rfi` or `src/lib/features/projects/rfi`). A child id matches that
|
|
294
352
|
importer's child id. A path matches when the importer file sits in that directory.
|
|
295
353
|
Bare names do not match. `*` is not a wildcard on this list. `default: "deny"` denies every
|
|
296
|
-
sibling crossing; the list cannot loosen it.
|
|
297
|
-
baseline
|
|
298
|
-
|
|
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
|
|
299
377
|
not include `reasonId`. `productHonesty.finished` stays false while any child wall is
|
|
300
378
|
advisory, including `{ "default": "advisory", "enforce": [...] }`. The list does not finish
|
|
301
379
|
the house. Doctor `slices` is `{ crossParent, crossSibling, pairs }` for the directed universe
|
|
302
380
|
pairs that occur. A child id that does not extend its universe id warns
|
|
303
381
|
`CONFIG_CHILD_SLICE_EXTENDS` and is treated as universe common. arkgate 4.8.22 and older
|
|
304
|
-
reject `childSlices
|
|
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
|
|
305
391
|
the string enum `deny | advisory` rejects the object at config load
|
|
306
392
|
(`must be one of deny, advisory` at `$.rules[n].childSlices.siblings`). It does not ignore
|
|
307
393
|
the object. No published release through 4.8.22 accepts `{ default, enforce }`. Ship that
|
|
@@ -326,10 +412,16 @@ Rule fields:
|
|
|
326
412
|
Ship it only on a release that includes this field.
|
|
327
413
|
`childSlices.sliceAliases` maps files that sit outside every slice folder onto
|
|
328
414
|
a child that already exists: `{ "from": "lib/compliance/**", "to": "features/projects/compliance" }`.
|
|
329
|
-
`
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
333
425
|
universe `sliceFolders` entry or a child `sliceFolders` entry is rejected
|
|
334
426
|
(`overlaps a slice folder`). Two aliases that can match one file are rejected
|
|
335
427
|
(`two slice aliases match the same file`). An aliased file takes the target
|
|
@@ -353,13 +445,20 @@ Rule fields:
|
|
|
353
445
|
It does not ignore the key. No published release through 4.8.22 accepts
|
|
354
446
|
`sliceAliases`. Ship a config that sets it only on a release that includes
|
|
355
447
|
this field. The write hook, ESLint, snippet analysis, ark-check, and CI all
|
|
356
|
-
see the per-edge decision.
|
|
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)).
|
|
357
452
|
|
|
358
453
|
Doctor may suggest moving a flat file that sits at universe level when
|
|
359
454
|
exactly one child slice imports it. The card names the file, that child, and
|
|
360
455
|
a destination folder inside the child that keeps the file's layer. It is a
|
|
361
456
|
suggestion. It is not a config field, not a finding, and not an ark-check
|
|
362
|
-
result.
|
|
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.
|
|
363
462
|
|
|
364
463
|
```jsonc
|
|
365
464
|
{
|
|
@@ -437,6 +536,30 @@ is evidence:
|
|
|
437
536
|
That pass runs in `ark-check` and CI. The write hook and ESLint see one edge at a time
|
|
438
537
|
and do not block it. arkgate 4.8.22 and older reject the value at config load (`must be
|
|
439
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`).
|
|
440
563
|
- `allowedCrossSlice` entries match a full slice id (`features/catalog`) or a bare slice name
|
|
441
564
|
(`catalog`), and only in the direction written. The reverse edge still denies. A bare name
|
|
442
565
|
matches that name under **any** slice folder, so in a repo with several slice parents
|
|
@@ -448,7 +571,10 @@ is evidence:
|
|
|
448
571
|
about your code) versus `unclassifiable path (src/widgets/x.tsx)` (a fact about our evidence).
|
|
449
572
|
`no slice folders` and `no path evidence` are the two remaining evidence reasons. A rule-level
|
|
450
573
|
`message` override no longer hides it: the reason is appended to your text, not replaced by it.
|
|
451
|
-
|
|
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`)
|
|
452
578
|
(`shared-roots-added`, `cross-slice-allowance-added`), so a policy review sees them. Both are
|
|
453
579
|
inert on a rule without `peerIsolation: true`, and policy-delta stays silent about them until
|
|
454
580
|
the wall exists.
|
|
@@ -464,7 +590,8 @@ other choice deliberately, not so slices can drift into a mesh.
|
|
|
464
590
|
runtime coupling. They still appear on the **violations** list with `typeOnly: true`,
|
|
465
591
|
`failsStrict: false`, and adapter diagnostic **severity: warning** so doctor/HTML keep
|
|
466
592
|
`violations.typeOnly` / `typeEdgePolicy` honest — but they **do not** fail merge/exit, library
|
|
467
|
-
`valid`, or preflight the way **value** edges do.
|
|
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
|
|
468
595
|
boundaries stay hard even for type-only. A value import of a pure-type barrel is still a value
|
|
469
596
|
edge (not soft-skipped). Prefer placing shared types in a **SharedTypes** (or owning) layer
|
|
470
597
|
both sides may import. Optional starter: [`templates/layers/shared-types.starter.json`](../templates/layers/shared-types.starter.json)
|
|
@@ -526,8 +653,8 @@ Each `arkrules/<Layer>.json` may declare:
|
|
|
526
653
|
|
|
527
654
|
| Section | Purpose | Modes | What it really enforces |
|
|
528
655
|
|---------|---------|--------|-------------------------|
|
|
529
|
-
| `structure[]` | Closed sensor ids (e.g. `orchestration-only`, `thin-adapter`, `writes-via-aggregate`, `aggregate-private-state`, `always-valid-factory`, `domain-event-on-mutation`, `no-anemic-model`) | `advisory` (default) or `enforced` | **Heuristics of module shape** — not proof that logic was extracted to Domain. `writes-via-aggregate` is driver-import + write-token in the declaring layer (ADR 0032). Tier-2 sensors (`no-anemic-model`) stay advisory-only (cannot promote to enforced). |
|
|
530
|
-
| `invariants[]` | Stable ids + description + `coverage` (`test` / `symbol`) + optional `appliesTo` globs | `advisory` or `enforced` | **Named policy + evidence.** `coverage.symbol` means a declaration of that identifier in a non-test file (witness path `symbolEvidenceFile
|
|
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.” |
|
|
531
658
|
|
|
532
659
|
**Reporting:** diagnostics carry `evidence.arkruleId` + `evidence.arkruleSource`. Label residual
|
|
533
660
|
**`[Layer]`** vs **`[ArkRules]`** in agent output. Doctor / HTML: `rulesUnderContract` (catalog +
|
|
@@ -571,6 +698,14 @@ npx ark-check --strict-config --policy-base ./before.ark.config.json --json
|
|
|
571
698
|
npx ark-check --strict-merge --policy-base-ref origin/main
|
|
572
699
|
```
|
|
573
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
|
+
|
|
574
709
|
The additive JSON result includes `policyDelta`: both policy hashes, the overall classification,
|
|
575
710
|
stable findings, and `blockingFindingIds`. Supported comparisons cover governed include/exclude
|
|
576
711
|
roots, layer patterns/exclusions/forbidden globals, deny rules, same-layer peer isolation,
|
|
@@ -627,14 +762,40 @@ type than product source:
|
|
|
627
762
|
|
|
628
763
|
| Check | What it does |
|
|
629
764
|
|-------|----------------|
|
|
630
|
-
| `ark-check --local --base origin/dev` | Opt-in local / multi-worktree cheap check. Same engine as `--changed`. Refused with `--strict-merge`. `ARK_CHECK_LOCAL=1` is the same unless CI
|
|
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. |
|
|
631
766
|
| `ark-check --changed --base origin/dev` | Layer check on touched sources only. A CSS/i18n PR pays almost nothing. |
|
|
632
767
|
| `ark-check --against origin/dev` | New violation keys vs **that ref's** baseline (not only HEAD). |
|
|
633
768
|
| `ark-check --contract-diff --base origin/dev` | Classifies tighten / loosen / reclassify / baseline-grow. |
|
|
634
|
-
| `--contract-session --author <id>` | Law-only PR. Mixed law+product still fails. Loosen/grow need a session even with an empty `stewards[]`; a non-empty list also needs a matching listed author. |
|
|
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. |
|
|
635
770
|
| `--persona touch\|contributor\|agent\|steward` | Budget presets for the same teeth. |
|
|
636
771
|
| `ark status --vs origin/dev` | One line: pin / contract / baseline drift vs that ref. |
|
|
637
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
|
+
|
|
638
799
|
Write-gate ApplyPatch denies a batch that mixes law files with product source. Humans who
|
|
639
800
|
never hit PreToolUse are unchanged. Local `pnpm` gates should call `--local --base` or
|
|
640
801
|
`--changed --base`, not only full-tree `--strict-merge`. Write hooks stay on the lexical
|
|
@@ -645,11 +806,20 @@ worktree has its own root); there is no machine-wide analysis lock.
|
|
|
645
806
|
include tree. File-local ArkRules sensors (class shape, orchestration-only, thin-adapter,
|
|
646
807
|
writes-via-aggregate) and hint preload stay on the touched set. Layer and cycle sensors
|
|
647
808
|
see that closure, so a new illegal import or a cycle the change can complete still
|
|
648
|
-
fails.
|
|
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
|
|
649
813
|
engine; not a second analysis path. A `--changed` pass is not a full-tree structural
|
|
650
814
|
verdict.
|
|
651
815
|
|
|
652
816
|
MCP clients can call `ark_policy_delta` with the previous `baseConfig`, an optional candidate
|
|
653
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.
|
|
654
824
|
It invokes the public classifier directly, is read-only, and marks a blocking result as an MCP
|
|
655
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`)
|
|
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
|
|