arkgate 4.8.3 → 4.8.4

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 (54) hide show
  1. package/CHANGELOG.md +242 -0
  2. package/README.md +10 -3
  3. package/bin/ark-check-runtime.mjs +340 -5
  4. package/bin/ark-layer-match.mjs +170 -13
  5. package/bin/ark-mcp-runtime.mjs +9 -2
  6. package/bin/lib/analysis-completeness.mjs +86 -0
  7. package/bin/lib/analysis-engine.mjs +5 -5
  8. package/bin/lib/architecture-scan.mjs +2 -0
  9. package/bin/lib/arkrules-contract.mjs +8 -1
  10. package/bin/lib/check-args.mjs +66 -0
  11. package/bin/lib/config-contract.mjs +26 -0
  12. package/bin/lib/design-smells.mjs +85 -0
  13. package/bin/lib/diagnostic-catalog.mjs +6 -1
  14. package/bin/lib/first-run-help.mjs +12 -0
  15. package/bin/lib/invariant-coverage-io.mjs +175 -19
  16. package/bin/lib/invariant-coverage.mjs +110 -7
  17. package/bin/lib/literal-path-drift-io.mjs +569 -0
  18. package/bin/lib/literal-path-drift.mjs +761 -0
  19. package/bin/lib/policy-delta-io.mjs +5 -0
  20. package/bin/lib/remediation.mjs +15 -0
  21. package/bin/lib/rules-under-contract.mjs +5 -0
  22. package/bin/lib/scan-files.mjs +54 -0
  23. package/bin/lib/sensor-promote-cli.mjs +372 -0
  24. package/bin/lib/sensor-promote-io.mjs +246 -0
  25. package/bin/lib/sensor-promotion.mjs +363 -0
  26. package/dist/{configTypes-dNJ2C0yx.d.ts → configTypes-dy5PfTqS.d.ts} +31 -0
  27. package/dist/{diagnosticCatalog-C5GgeyEE.d.ts → diagnosticCatalog-DgTs0abp.d.ts} +75 -7
  28. package/dist/eslint/index.cjs +6 -6
  29. package/dist/eslint/index.d.ts +34 -1
  30. package/dist/eslint/index.js +6 -6
  31. package/dist/index.cjs +32 -32
  32. package/dist/index.d.ts +65 -4
  33. package/dist/index.js +29 -29
  34. package/dist/nestjs/index.cjs +5 -5
  35. package/dist/nestjs/index.d.ts +3 -3
  36. package/dist/nestjs/index.js +5 -5
  37. package/dist/runtime/index.cjs +15 -15
  38. package/dist/runtime/index.d.ts +6 -6
  39. package/dist/runtime/index.js +15 -15
  40. package/dist/{types-dK24fDZa.d.ts → types-BuM8WNqe.d.ts} +1 -1
  41. package/dist/{types-DeK7SYGC.d.ts → types-D95drJ3_.d.ts} +1 -1
  42. package/docs/README.md +1 -1
  43. package/docs/agent-guide.md +182 -0
  44. package/docs/configuration.md +77 -1
  45. package/docs/develop.md +1 -0
  46. package/docs/diagnostics.md +70 -1
  47. package/docs/package-surface.md +32 -2
  48. package/package.json +2 -2
  49. package/schemas/ark.config.schema.json +63 -0
  50. package/server.json +3 -3
  51. package/templates/agent-skills/ark-adopt/SKILL.md +5 -0
  52. package/templates/agent-skills/ark-coverage/SKILL.md +1 -0
  53. package/templates/skills/ark-adopt.md +5 -0
  54. package/templates/skills/ark-coverage.md +1 -0
@@ -1,4 +1,4 @@
1
- import { a as ArkConfigRule, f as ArkConfigLayer, A as ArkConfig } from './configTypes-dNJ2C0yx.js';
1
+ import { a as ArkConfigRule, f as ArkConfigLayer, A as ArkConfig } from './configTypes-dy5PfTqS.js';
2
2
 
3
3
  /**
4
4
  * Policy types for the Ark kernel.
@@ -1,4 +1,4 @@
1
- import { i as Policy, P as PolicyViolation, I as IntentName, j as IntentCreator, k as IntentRelationship, b as ArchitectureProfile, D as DomainEvent, E as EventMetadata, h as PolicyEnforcementMode, A as ArchitectureLayer, c as ArchitectureRule, d as ArkCheckConfig } from './types-dK24fDZa.js';
1
+ import { i as Policy, P as PolicyViolation, I as IntentName, j as IntentCreator, k as IntentRelationship, b as ArchitectureProfile, D as DomainEvent, E as EventMetadata, h as PolicyEnforcementMode, A as ArchitectureLayer, c as ArchitectureRule, d as ArkCheckConfig } from './types-BuM8WNqe.js';
2
2
 
3
3
  /**
4
4
  * PolicyEngine
package/docs/README.md CHANGED
@@ -62,7 +62,7 @@ These are **not** the day-to-day product path. They stay in the repo for evidenc
62
62
  | Field adoption kit (scaffolding, not closed) | [field/](field/) |
63
63
  | Runtime hardening (experimental) | [production-hardening.md](production-hardening.md) |
64
64
 
65
- Current published: [releases/4.8.3.md](releases/4.8.3.md) (`arkgate@4.8.3` on npm `latest`; does not close `K01`).
65
+ Current published: [releases/4.8.3.md](releases/4.8.3.md) (`arkgate@4.8.3` on npm `latest`; does not close `K01`). Current tree: [releases/4.8.4.md](releases/4.8.4.md) (pending publish).
66
66
  Prior: [releases/4.8.2.md](releases/4.8.2.md) · [releases/4.8.1.md](releases/4.8.1.md) · [4.8.0](releases/4.8.0.md) · [4.7.6](releases/4.7.6.md) · [4.7.5](releases/4.7.5.md) · [4.7.4](releases/4.7.4.md) · [4.7.3](releases/4.7.3.md) · [4.7.2](releases/4.7.2.md) · [4.7.1](releases/4.7.1.md) · [4.7.0](releases/4.7.0.md) · [4.6.7](releases/4.6.7.md) · [4.6.6](releases/4.6.6.md) · [4.6.5](releases/4.6.5.md) · [4.6.4](releases/4.6.4.md) · [4.6.3](releases/4.6.3.md) · [4.6.2](releases/4.6.2.md) · [4.6.1](releases/4.6.1.md) · [4.6.0](releases/4.6.0.md).
67
67
  Older notes: [releases/](releases/). Config: [configuration.md](configuration.md).
68
68
 
@@ -461,6 +461,23 @@ order (universal): `lint` → `typecheck` → `arkgate-check` / `check:architect
461
461
  When `include` matches **zero** TS/JS files, plan/doctor treat that as **not done**
462
462
  (`goal.emptyScope`, adoption gap `empty-scope`) — never “clean architecture.”
463
463
 
464
+ The **verdict path refuses** in that state rather than passing: a plain or `--strict`
465
+ `ark-check` over zero governed files exits 1 with `ANALYSIS_COVERS_NO_FILES`, because
466
+ every rule is vacuously satisfied on an empty set. It fires when source exists under the
467
+ analyzed root and the contract governs none of it, or when the analyzed root is not the
468
+ root you asked for (a contract found outside `--root` makes ArkGate adopt the contract's
469
+ directory). A genuinely greenfield repo — no governable source anywhere under the root
470
+ you asked for — still passes, so `ark init` can land a contract before the code.
471
+
472
+ "Source exists" is answered by a probe the contract cannot steer: it ignores `exclude`
473
+ (otherwise `exclude: ["**"]` would buy a green), skips dot-directories, never follows a
474
+ symlink, skips `*.config.*` (a repo whose only TS is `vite.config.ts` is greenfield, not
475
+ a mismatch), and stops at 200 files — the message says *at least N* when it did.
476
+
477
+ The commands below are exempt on purpose: they are how the refusal gets diagnosed and
478
+ fixed, so `--plan`, `--coverage` and `--doctor` still exit 0 on an empty scope and report
479
+ `empty-scope`. **Do not gate CI on a report mode** — gate on `ark-check` / `--strict`.
480
+
464
481
  ```bash
465
482
  npx ark-check --suggest-include --json # workspaces + nested package.json+TS roots
466
483
  npx ark-check --adopt-contract --write # expand include + UI patterns (no rule weakening)
@@ -469,6 +486,167 @@ npx ark-check --coverage
469
486
 
470
487
  Polyglot repos: Ark only governs TypeScript/JS. Point include at package roots that have sources.
471
488
 
489
+ ### Which rules can be enforced (`--sensors`, `--promote`)
490
+
491
+ Promotion — moving a rule from `advisory` to `enforced` — used to be discovered
492
+ by trial. Edit the ArkRules JSON, wait for a full run (~160s on a real
493
+ repository), read the result, `git checkout` it back. Four attempts before the
494
+ map was clear, and one of them ended in a rejection naming a sensor id the
495
+ author had never typed: a rule called `types-only` refused with *"sensor
496
+ `no-anemic-model` is Tier-2 advisory-only"*.
497
+
498
+ Both surfaces are read-only by default and neither invents a second opinion:
499
+ they project the same declarations the gate reads.
500
+
501
+ ```bash
502
+ npx ark-check --sensors [--json] # the map: what can ever be enforced
503
+ npx ark-check --promote [--json] # the price: what enforcing would cost, one run
504
+ npx ark-check --promote <ruleId> --apply # write mode "enforced" into that rule's own file
505
+ ```
506
+
507
+ **`--sensors`** lists every sensor ArkGate ships — ArkRules, ArkRun and
508
+ ArkOrder — with its plane, its tier and whether it can *ever* be enforced, so
509
+ Tier-2 shows up before you write the rule rather than after you wait for a run.
510
+ It also says *how*: only the ArkRules plane is promoted per rule; ArkRun and
511
+ ArkOrder are switched by the plane-level `arkRun.mode` / `arkOrder.mode`, and
512
+ `--promote --apply` writes ArkRules documents only.
513
+ Underneath it lists every rule the contract actually declares, each with its
514
+ local id, the sensor it delegates to, the layer, the file it was declared in,
515
+ its current mode, and the reason it can or cannot be promoted. Three things
516
+ block a promotion, and the surface names which one fired:
517
+
518
+ - **`tier-2-advisory-only`** — the sensor is a heuristic (`no-anemic-model`,
519
+ `arkrun-skip-resolve`). Advisory forever; the contract rejects `enforced`.
520
+ - **`no-structure-teeth`** — `invariant-coverage` as a *structure* entry emits
521
+ nothing (coverage is judged per entry in `invariants`), so enforcing it would
522
+ change nothing. Promote the invariant instead.
523
+ - **`no-coverage-evidence`** — an invariant whose evidence does not support
524
+ promotion. The text is `canPromoteInvariant`'s own, so this surface can never
525
+ promise a promotion the gate then refuses.
526
+
527
+ It needs no TypeScript and runs no analysis: the contract, the ArkRules
528
+ documents it points at, and the coverage evidence walk (a filesystem walk plus a
529
+ text match — ArkGate never executes a test). Exit 0 on a report, **2** when the
530
+ contract or its ArkRules references will not load, or when the governed-file
531
+ scan itself fails — reporting every invariant as uncovered because ArkGate could
532
+ not collect the inputs would be our limitation printed as a fact about your
533
+ tests.
534
+
535
+ **`--promote`** adds the price. Advisory rules are already evaluated on every
536
+ run, so the findings each one produces are sitting in the analysis that just
537
+ happened, stamped with the rule that produced them: **one** run prices **every**
538
+ declared rule, which is the whole difference from the loop it replaces. A
539
+ promotable advisory rule with zero findings today is a free promotion; one with
540
+ seven is seven findings that stop being warnings and start failing the gate.
541
+
542
+ Findings are counted per `<sourceFile>#<ruleId>`, not per bare id. Rule ids are
543
+ unique inside one ArkRules document, not across them, so two layer files may
544
+ both declare `shared-id` — keyed on the id alone their findings pool and each
545
+ row reports the other's as its own.
546
+
547
+ **A price the run could not measure is never printed as a price.** Two things
548
+ qualify the numbers, and both are named above them rather than left implied:
549
+
550
+ - **Incomplete analysis.** Parse failures suppress findings, so the count is a
551
+ floor, not the cost.
552
+ - **The classification floor.** Below it every enforced ArkRules finding is
553
+ demoted to a warning, so promoting buys a label and not a tooth — the gate
554
+ would still pass. `wouldBlock` drops to zero and the run says so. Classify
555
+ more of the tree (`--coverage`) before promoting.
556
+
557
+ Plan by default — there is no `--dry-run` anywhere in `bin/`. `--apply` needs
558
+ one named rule id: `--promote <ruleId> --apply`, or `--promote=<ruleId>` when
559
+ the id starts with `-`. It refuses a bare `--promote --apply` rather than
560
+ rewriting the contract in bulk behind a single flag, refuses an id declared in
561
+ two documents rather than silently writing the first, and refuses to make a
562
+ contract change on a cost this run did not measure. `--promote` cannot be
563
+ combined with a mode that answers first (`--sensors`, `--coverage`, `--plan`,
564
+ `--doctor`, …) — that printed the report and exited 0 having written nothing —
565
+ nor with `--changed` / `--against` / `--baseline`, which narrow or suppress the
566
+ findings the price is made of.
567
+
568
+ The write binds to the rule that was priced (an edit that changed its sensor in
569
+ between is refused), writes every byte before it truncates so a failed write
570
+ cannot leave the project without a loadable contract, and names its refusals:
571
+ `symlink`, `hard-link`, `not-utf8`, `outside-root`, `short-write`. A document
572
+ that was already indented keeps its indentation and trailing newline; a minified
573
+ one comes back pretty-printed, because the write is a JSON round-trip rather
574
+ than a targeted text edit. Exit 0 for a preview or a successful write, 1 for an
575
+ unknown rule id or a refused write, 2 for bad arguments or ArkRules references
576
+ that will not load.
577
+
578
+ ### Literal path drift after a rename (`--path-drift`)
579
+
580
+ A repo path written inside a **string, a comment or a docstring** is invisible to
581
+ the rest of the gate: `tsc` resolves imports, not strings, and ESLint does not
582
+ either. A rename therefore compiles green and the reference lies afterwards.
583
+
584
+ ```bash
585
+ npx ark-check --path-drift --base-ref origin/main # preview
586
+ npx ark-check --path-drift --base-ref origin/main --write # apply the anchored fixes
587
+ npx ark-check --path-drift --base-ref origin/main --all # + the unanchored sweep
588
+ ```
589
+
590
+ Two modes, because they make different claims:
591
+
592
+ - **Anchored** (default) — the referenced path is gone and exactly one rename in
593
+ `git diff --find-renames <base-ref>` says where it went. A finding
594
+ (`LITERAL_PATH_DRIFT`) normally carries a replacement written in the author's
595
+ own form (alias stays alias, relative is recomputed relative, a path with no
596
+ include-root prefix keeps its coordinate space); the fix is one-directional
597
+ and `--write` applies it. Exit 1 while anchored drift remains. Three things
598
+ must hold before a replacement is offered at all: a rename explains the
599
+ reference, the destination itself resolves (otherwise the "fix" only moves the
600
+ drift), and the destination is path-shaped — a git path is raw bytes, and a
601
+ destination containing a quote or a newline would edit the program rather than
602
+ repair a reference. A finding that clears the first two but whose destination
603
+ leaves its own alias root is reported with the target only and marked *rewrite
604
+ by hand*; `--write` never touches it, so the summary counts writable
605
+ replacements separately.
606
+ - **Unanchored** (`--all`) — a literal that looks like a repo path and does not
607
+ resolve, with nothing to say where it went (`LITERAL_PATH_UNRESOLVED`).
608
+ Advisory: never written, never fails a run. It is opt-in because ArkGate
609
+ cannot tell a dead reference from an illustrative one, and reporting the
610
+ difference as if it could would be our limitation stated as a fact about your
611
+ code. The **count is always printed**, listed or not.
612
+
613
+ Three exit codes, so a pipeline can tell the three outcomes apart from the
614
+ status alone:
615
+
616
+ | exit | meaning |
617
+ | --- | --- |
618
+ | `0` | anchored mode ran and nothing is left |
619
+ | `1` | anchored drift remains |
620
+ | `2` | anchored mode could not run (no usable base ref) — this run proved nothing |
621
+
622
+ A green tick is printed only for `0`. With no usable base ref the run prints
623
+ `○ Anchored mode did not run` and exits `2`: a tick, or a zero status, over a
624
+ check that never happened is the false green this pass exists to remove.
625
+
626
+ `--write` refuses rather than risks the file, and every refusal is named in the
627
+ output: a symlinked leaf or parent (`symlink`), a hard link to a file the repo
628
+ does not own (`hard-link`), content that is not valid UTF-8 (`not-utf8` — a
629
+ whole-file rewrite would replace the offending byte with U+FFFD), a token that
630
+ has moved since the scan (`token-moved`), and anything resolving outside the
631
+ root (`outside-root`). The read-modify-write goes through a single
632
+ `O_NOFOLLOW` descriptor, so the path is never resolved twice.
633
+
634
+ The rename set is taken against the working tree, so a rename that is staged
635
+ but not yet committed is covered — the moment the drift is cheapest to fix. (A
636
+ bare `mv` without `git add` is invisible to rename detection: its destination is
637
+ untracked.) Without a usable base ref the
638
+ run says so instead of printing a green.
639
+
640
+ The pass reads the contract for one thing only — `include`, to learn which
641
+ prefixes a path may be written under — and ignores `exclude`: a contract must not
642
+ be able to hide drift from the pass that reports it, and a contract too broken to
643
+ parse is no reason to stop looking either. Scope: every text format where a repo
644
+ path is written by hand (`.ts .tsx .mts
645
+ .cts .js .jsx .mjs .cjs .css .scss .json .md`) — deliberately wider than the
646
+ TS/TSX gate the type-aware passes use, because a comment is not code and the
647
+ class was first found in a `.css` file. Generated files are skipped, and every
648
+ file the walk refuses is counted by reason in the output.
649
+
472
650
  ### Presets
473
651
 
474
652
  - `hexagonal` / `layered` / `feature-sliced` / `monorepo` / **`ui-surface`** (UI/Vite/Remotion-style) / **`vertical-slice`** (features/* + peerIsolation) / **`ddd-bounded-contexts`** (contexts/*/domain|application|infra + shared kernel)
@@ -899,6 +1077,10 @@ edges are not denied by that rule.
899
1077
  - **Allowed:** same-slice imports when both paths classify; classic non-peerIsolation denies still apply across layers.
900
1078
  - **`sliceFolders`:** optional parent segments (default: inferred from layer globs).
901
1079
  - **Fail-closed:** missing paths, empty/unresolvable slice folders, or unclassifiable either side → **deny** via peerIsolation (cannot prove same-slice).
1080
+ - **`sharedRoots`** (4.8.4): roots the repo declares shared on purpose (`["ui", "hooks", "lib/permissions"]`). A file under a declared shared root is evidence, not an unclassifiable path, so fail-closed stops firing on every shared file. **Anchored** — the root starts the path, optionally after one `src/` or `app/`; write deeper or monorepo roots out (`packages/web/src/ui`) or glob them, and a bare `*` / `**` is refused. A path that still resolves to a slice keeps its slice.
1081
+ - **`allowedCrossSlice`** (4.8.4): `[{ "from": "features/checkout", "to": "features/catalog" }]` — one directed slice→slice edge the repo declares on purpose. The reverse still denies.
1082
+ - **The denial names its reason:** `cross-slice edge a → b` (a fact about the code) vs `unclassifiable path (…)`, `no slice folders`, `no path evidence` (facts about the evidence ArkGate had).
1083
+ - Promoting a genuinely shared slice to its own layer remains the recommended model; the two declarations exist so ArkGate can enforce a repo that deliberately chose otherwise.
902
1084
  - Enforced by `ark-check`, `arkgate/eslint`, and `ark-mcp` (path-aware edges and path-less intent refs share the same SoT).
903
1085
  - Fixes are **judgment** (not mechanical-safe).
904
1086
 
@@ -86,6 +86,33 @@ Top-level fields:
86
86
  - `include`, `exclude`, `excludeGenerated`, `frameworkOverlay`
87
87
  - `layers`, `rules`, `cyclePolicy`
88
88
  - `dynamicImportAllowlist`, `safety`
89
+ - **`coverage`** (optional) — invariant-coverage scan controls: `testGlobs` (globs that decide
90
+ which files count as tests, replacing the built-in `*.test.*` / `tests/` name heuristic),
91
+ `maxFiles` (evidence file budget, default `400`) and `coverageRoots` (path prefixes where the
92
+ project declares its runner actually executes tests). Absence is silent and changes no verdict.
93
+ Unknown keys fail closed. When the budget is hit, `INVARIANT_UNCOVERED` reports the numbers
94
+ (files loaded, tests retained, files discarded at the cap) and names `coverage.maxFiles` as
95
+ the knob that raises it — coverage never claims "never had tests" because of our own cap.
96
+ `maxFiles` is clamped to a hard ceiling of 20000 (the config validator has no
97
+ `maximum` keyword, so a schema bound would be accepted and then ignored). The cap
98
+ bounds files **retained as evidence**, not files opened: a test is read before it can
99
+ be judged for naming an invariant, so the diagnostic reports files read alongside
100
+ files retained.
101
+ Nothing is dropped in silence: files past the budget, files over the 256KB per-file cap,
102
+ unreadable files or directories (permissions, broken symlinks), directories past the walk depth
103
+ limit (8), symlinks whose target resolves outside the project root, and tests naming no
104
+ catalogued invariant are each counted and named in the diagnostic. A symlinked test is read only
105
+ when its target is inside the root: a file that is not in this repo never proves an invariant
106
+ covered.
107
+
108
+ `coverageRoots` closes a false green ArkGate could otherwise produce. Coverage is proven by
109
+ matching an invariant id in a test title — a filesystem walk plus a text match. **ArkGate never
110
+ executes tests and never reads a runner config**, so a test in a folder no runner runs certifies
111
+ the invariant just as well as one that runs. Declaring `coverageRoots` gives ArkGate a second
112
+ declaration to compare the first against: when the only covering test falls outside them, it
113
+ reports `INVARIANT_COVERAGE_OUTSIDE_ROOTS` (advisory) and refuses to promote that invariant to
114
+ `enforced`. Declaring nothing keeps the old silence — without a declaration there is nothing to
115
+ compare, and ArkGate makes no claim about where tests run.
89
116
  - **`arkRules`** (optional, schema `1.1+`) — map of layer name → project-relative path to an
90
117
  ArkRules file (e.g. `"DomainModel": "arkrules/DomainModel.json"`). Keys must match a declared
91
118
  layer. Missing/invalid referenced files **fail closed**.
@@ -130,11 +157,60 @@ process module-capability family must be denied.
130
157
 
131
158
  Rule fields:
132
159
 
133
- - `from`, `to`, `allowed`, `message`, `peerIsolation`, `sliceFolders`
160
+ - `from`, `to`, `allowed`, `message`, `peerIsolation`, `sliceFolders`, `sharedRoots`,
161
+ `allowedCrossSlice`
134
162
  - `peerIsolation: true` + `allowed: false`: deny only when slice ids differ; same-slice allows
135
163
  when both paths classify. Missing paths, empty slice folders, or unclassifiable slices
136
164
  **fail closed** (deny — cannot prove same-slice).
137
165
 
166
+ #### Declared peerIsolation exceptions (4.8.4)
167
+
168
+ Fail-closed denies on **absence of evidence**, so in a repo that legitimately keeps shared code
169
+ outside `features/<slice>/` (a `ui/`, `hooks/`, `lib/permissions/` tree) every shared file reads as
170
+ a violation — thousands of them, none a real cross-slice import. That is ArkGate reporting *our*
171
+ inability to place a file as a fact about *your* code. Two declarations fix it, and a declaration
172
+ is evidence:
173
+
174
+ ```jsonc
175
+ {
176
+ "from": "Features", "to": "Features", "allowed": false, "peerIsolation": true,
177
+ "sliceFolders": ["features"],
178
+ // Roots this repo keeps shared on purpose — no longer "unclassifiable".
179
+ "sharedRoots": ["ui", "layout", "providers", "hooks", "lib/permissions"],
180
+ // Directed slice→slice edges this repo wants. One entry = one direction.
181
+ "allowedCrossSlice": [{ "from": "features/checkout", "to": "features/catalog" }]
182
+ }
183
+ ```
184
+
185
+ - `sharedRoots` is **anchored**: the root must start the repo-relative path, optionally after a
186
+ single conventional source folder (`src/`, `app/`). So `ui` covers `ui/button.tsx` and
187
+ `src/ui/button.tsx` but **not** `modules/a/ui/x.tsx` — an unanchored root would exempt a whole
188
+ tree you never declared. Write a deeper or monorepo root out (`packages/web/src/ui`) or glob it
189
+ (`packages/*/src/ui`). Matching is case-insensitive; a bare `*` or `**` is refused, because one
190
+ character must not disable fail-closed. A path that still resolves to a slice keeps its slice —
191
+ `features/auth/ui/form.tsx` stays `features/auth` — so a shared root can never launder a real
192
+ cross-slice edge.
193
+ - `allowedCrossSlice` entries match a full slice id (`features/catalog`) or a bare slice name
194
+ (`catalog`), and only in the direction written. The reverse edge still denies. A bare name
195
+ matches that name under **any** slice folder, so in a repo with several slice parents
196
+ (`features/auth` and `modules/auth`) write the full id — `features/auth` — or the
197
+ declaration allows more edges than you meant.
198
+ - Everything else is unchanged: two different slices with nothing declared still deny, and a file
199
+ that is neither in a slice nor under a declared shared root still **fails closed**.
200
+ - The denial now names which reason fired — `cross-slice edge features/a → features/b` (a fact
201
+ about your code) versus `unclassifiable path (src/widgets/x.tsx)` (a fact about our evidence).
202
+ `no slice folders` and `no path evidence` are the two remaining evidence reasons. A rule-level
203
+ `message` override no longer hides it: the reason is appended to your text, not replaced by it.
204
+ - Both declarations are **weakening** changes in `ark policy-delta`
205
+ (`shared-roots-added`, `cross-slice-allowance-added`), so a policy review sees them. Both are
206
+ inert on a rule without `peerIsolation: true`, and policy-delta stays silent about them until
207
+ the wall exists.
208
+
209
+ **The recommended model is still to promote a genuinely shared slice to its own layer** and let
210
+ the layer edges carry it: a one-way peer import between slices is a DAG the layer graph cannot
211
+ see. `sharedRoots` and `allowedCrossSlice` exist so ArkGate can enforce a design that made the
212
+ other choice deliberately, not so slices can drift into a mesh.
213
+
138
214
  ### Type-only edges (placement debt)
139
215
 
140
216
  `import type` / `export type` and pure type-only named bindings are **type placement debt**, not
package/docs/develop.md CHANGED
@@ -86,6 +86,7 @@ can show `runtimeObserved: true` even when branch-protection policy is plan-unav
86
86
  | Layers, rules, globs | [configuration.md](configuration.md) · `ark.config.json` |
87
87
  | ArkRules (structure + invariants) | [configuration.md](configuration.md#arkrules-intra-layer-opt-in) · `arkRules` / `arkrules/*` · skill `/ark-adopt` (session 0) or `/ark-autopilot` |
88
88
  | Rules inventory (brownfield) | `arkgate-check --rules-inventory` · MCP `ark_rules_inventory` |
89
+ | Which rules can be enforced | `arkgate-check --sensors` (the map) · `arkgate-check --promote [<ruleId>] [--apply]` (the price) · [agent-guide](agent-guide.md#which-rules-can-be-enforced---sensors---promote) |
89
90
  | Stable package API | [package-surface.md](package-surface.md) |
90
91
  | Diagnostic codes (`ruleId` why/fix) | [diagnostics.md](diagnostics.md) · root `DIAGNOSTIC_CATALOG` |
91
92
  | Session / project status snapshot | `ark status --json` · MCP `ark_status` · [session recipe](agent-guide.md#session-recipe-agent-turn) · schema `arkgate/schema/status-manifest` |
@@ -44,6 +44,7 @@ Link form for agents: `docs/diagnostics.md#RULE_ID` (exact-case HTML anchors bel
44
44
  | [`ARKRULE_INVARIANT`](#ARKRULE_INVARIANT) | arkrules | ArkRule invariant failed |
45
45
  | [`ARKRULE_SCOPE_EMPTY`](#ARKRULE_SCOPE_EMPTY) | arkrules | ArkRule appliesTo matched zero files |
46
46
  | [`INVARIANT_UNCOVERED`](#INVARIANT_UNCOVERED) | arkrules | Invariant without coverage evidence |
47
+ | [`INVARIANT_COVERAGE_OUTSIDE_ROOTS`](#INVARIANT_COVERAGE_OUTSIDE_ROOTS) | arkrules | Covering test outside the declared coverage roots |
47
48
  | [`ARKRUN_MISSING_ROOT`](#ARKRUN_MISSING_ROOT) | arkrun | No kernel factory in composition roots |
48
49
  | [`ARKRUN_KERNEL_IN_DOMAIN`](#ARKRUN_KERNEL_IN_DOMAIN) | arkrun | Domain-role layer imports the kernel |
49
50
  | [`ARKRUN_DIRECT_NEW`](#ARKRUN_DIRECT_NEW) | arkrun | Managed type constructed with new |
@@ -69,7 +70,10 @@ Link form for agents: `docs/diagnostics.md#RULE_ID` (exact-case HTML anchors bel
69
70
  | [`DESIGN_SMELL_REGRESSION`](#DESIGN_SMELL_REGRESSION) | preflight | Design smell regression on base-relative ratchet |
70
71
  | [`ANALYSIS_PARSE_INCOMPLETE`](#ANALYSIS_PARSE_INCOMPLETE) | analysis | Parse incomplete |
71
72
  | [`LEXICAL_EVIDENCE_INCOMPLETE`](#LEXICAL_EVIDENCE_INCOMPLETE) | analysis | Lexical evidence incomplete |
73
+ | [`ANALYSIS_COVERS_NO_FILES`](#ANALYSIS_COVERS_NO_FILES) | analysis | Analysis covered no files |
72
74
  | [`ANALYSIS_HOST_UNAVAILABLE`](#ANALYSIS_HOST_UNAVAILABLE) | analysis | Analysis host unavailable |
75
+ | [`LITERAL_PATH_DRIFT`](#LITERAL_PATH_DRIFT) | drift | Literal path moved by a rename |
76
+ | [`LITERAL_PATH_UNRESOLVED`](#LITERAL_PATH_UNRESOLVED) | drift | Literal path does not resolve |
73
77
  | [`ADAPTER_NOT_ALLOWED_FOR_PORT`](#ADAPTER_NOT_ALLOWED_FOR_PORT) | adapter | Adapter not allowed for port |
74
78
  | [`FORBIDDEN_PATTERN`](#FORBIDDEN_PATTERN) | snippet-policy | Forbidden regex pattern |
75
79
  | [`FORBIDDEN_SUBSTRING`](#FORBIDDEN_SUBSTRING) | snippet-policy | Forbidden substring |
@@ -281,7 +285,18 @@ Link form for agents: `docs/diagnostics.md#RULE_ID` (exact-case HTML anchors bel
281
285
  **Invariant without coverage evidence**
282
286
 
283
287
  - **Why:** An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial). Kind is `never-had-tests` (adopt residual) vs `tests-disappeared` (suite exists).
284
- - **Fix:** Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green.
288
+ - **Fix:** Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green. When the message reports an exhausted file budget, raise `coverage.maxFiles` (or narrow `coverage.testGlobs`) in ark.config.json. The message also names every file the scan discarded and why (budget, per-file byte cap, unreadable, walk depth limit, symlink resolving outside the project root, no catalogued invariant named) — nothing is dropped in silence.
289
+
290
+ <a id="INVARIANT_COVERAGE_OUTSIDE_ROOTS"></a>
291
+
292
+ ### `INVARIANT_COVERAGE_OUTSIDE_ROOTS`
293
+
294
+ **Covering test outside the declared coverage roots** · often advisory
295
+
296
+ - **Why:** The only test naming this invariant sits outside `coverage.coverageRoots` — the places the project declares its runner executes. ArkGate matches declared text and never executes tests, so it cannot tell whether that file is ever run: coverage there is a test that *exists*, not a test that *runs*.
297
+ - **Fix:** Move the test under a declared coverage root, or add its root to `coverage.coverageRoots` in ark.config.json. Advisory: it never fails strict, but promotion to enforced refuses on it.
298
+
299
+ Declaring nothing is silent — without `coverage.coverageRoots` there is no second declaration to compare against, and ArkGate makes no claim about where tests run.
285
300
 
286
301
  ## ArkRun (opt-in extra)
287
302
 
@@ -530,6 +545,15 @@ Haken slaving: few slow keys (ξ) determine derived fast state. Field ingest nev
530
545
  - **Why:** Single-file validation cannot prove project module resolution. The write hook is already the verdict.
531
546
  - **Fix:** Re-run `npx arkgate-check --root . --config ark.config.json`, or treat the hook deny as final. Do not call `ark_prepare_change` from a hook deny.
532
547
 
548
+ <a id="ANALYSIS_COVERS_NO_FILES"></a>
549
+
550
+ ### `ANALYSIS_COVERS_NO_FILES`
551
+
552
+ **Analysis covered no files**
553
+
554
+ - **Why:** No file matched the contract `include` and layer patterns under the analyzed root, so the run had nothing to check. Every rule is vacuously satisfied on an empty set: a green here would read exactly like a green over a governed tree while certifying nothing. Usual causes are a `--root` that is not the tree the contract describes (including a contract found outside the requested root, whose directory is then adopted as the project root), `include` / `exclude` patterns that match nothing, or layer patterns written for a different layout.
555
+ - **Fix:** Point `--root` at the tree the contract describes, or keep the contract inside that tree, or fix the `include` / `exclude` / layer patterns so they match real files — then re-run `npx arkgate-check --root . --config ark.config.json`. `--plan` and `--coverage` report the empty scope without refusing, and `--adopt-contract --write` proposes an `include` that matches the tree. This is a refusal about ArkGate's own inputs, not a finding about your code; no baseline or policy acknowledgement can suppress it, and `exclude` cannot silence it — the "is there source here" probe deliberately ignores the contract's own `exclude`, skips dot-directories, never follows a symlink, and skips `*.config.*` tooling files so a polyglot repo is not mistaken for a mismatch.
556
+
533
557
  <a id="ANALYSIS_HOST_UNAVAILABLE"></a>
534
558
 
535
559
  ### `ANALYSIS_HOST_UNAVAILABLE`
@@ -539,6 +563,51 @@ Haken slaving: few slow keys (ξ) determine derived fast state. Field ingest nev
539
563
  - **Why:** No usable TypeScript / analysis host was available for this invocation.
540
564
  - **Fix:** Install a supported TypeScript version visible to the project, then re-run. Unavailable analysis is fail-closed.
541
565
 
566
+ ## Literal path drift
567
+
568
+ Reported by `arkgate-check --path-drift` only — this pass is not part of the
569
+ architecture verdict. A path written inside a string, a comment or a docstring
570
+ is invisible to the rest of the gate: `tsc` resolves imports, not strings, and
571
+ ESLint does not either, so a rename compiles green and the reference lies
572
+ afterwards. Field data from a 783-rename migration found the drift in four
573
+ forms — the tsconfig alias, a relative literal, a path written without the
574
+ include-root prefix, and prose (the largest class, and the only one with no
575
+ detector anywhere; it turned up in `.ts`, `.tsx` and `.css`).
576
+
577
+ The pass reads every text format where a repo path is written by hand
578
+ (`.ts .tsx .mts .cts .js .jsx .mjs .cjs .css .scss .json .md`), skips generated
579
+ files, and reports every file it refused to read, by reason.
580
+
581
+ <a id="LITERAL_PATH_DRIFT"></a>
582
+
583
+ ### `LITERAL_PATH_DRIFT`
584
+
585
+ **Literal path moved by a rename**
586
+
587
+ - **Why:** A repo path written inside a string, a comment or a docstring no longer resolves, and the rename set says where it went. Nothing in the gate sees this class: `tsc` resolves imports, not strings, and ESLint does not either, so the rename compiles green and the reference lies afterwards. It appears in four forms — the tsconfig alias, a relative literal, a path written without the include-root prefix, and prose — and a hand sweep reliably covers one of them.
588
+ - **Fix:** Apply the suggested replacement, or re-run `npx arkgate-check --path-drift --base-ref <ref> --write` to apply every writable anchored replacement at once. The rewrite is mechanical and one-directional: the destination comes from the rename, it must itself resolve and be path-shaped, and the token is rewritten in the form the author wrote it in. A destination that leaves the alias root of the literal is reported with the target only and must be rewritten by hand.
589
+
590
+ Only a rename whose source really is gone and that has exactly one destination
591
+ may anchor a finding. A source that maps to two destinations is not a
592
+ one-directional fix, so it anchors nothing and its references fall through to
593
+ the advisory list below; the count is printed.
594
+
595
+ <a id="LITERAL_PATH_UNRESOLVED"></a>
596
+
597
+ ### `LITERAL_PATH_UNRESOLVED`
598
+
599
+ **Literal path does not resolve** · often advisory
600
+
601
+ - **Why:** A literal that looks like a repo path does not resolve under this root, and no rename explains where it went. Unlike `LITERAL_PATH_DRIFT` this is a candidate, not a verdict: with nothing to anchor it, ArkGate cannot tell a dead reference from an illustrative path in a comment, an example in documentation, or a path belonging to another tree.
602
+ - **Fix:** Read the candidate and decide: fix the path, or leave it. Advisory only — it never fails a run and is never rewritten by `--write`, because there is no destination to propose. Run `--path-drift --all` to list the sweep.
603
+
604
+ The sweep is opt-in for exactly the reason the coverage budget is reported
605
+ rather than hidden: on a repository that *writes about* paths it produced 4085
606
+ candidates out of 9536 literals, nearly all of them illustrative. Listing that
607
+ by default would be ArkGate's inability to resolve a string presented as a fact
608
+ about your code. The count is always printed, so opting out of the list is
609
+ never opting out of knowing.
610
+
542
611
  ## Port adapters
543
612
 
544
613
  <a id="ADAPTER_NOT_ALLOWED_FOR_PORT"></a>
@@ -35,7 +35,7 @@ hardening guide remains repository-hosted rather than duplicated in the gate tar
35
35
  | **Field upgrade truth (4.5.6)** | `ark upgrade` registry-aware install; JSON `reasonCode` / `suggestedInstallCmd`; `skillDrift`; `--refresh-skills`; `postUpgradeChecks`; `hostSelection`. | No false-skip when registry ahead; offline honesty; customized skills preserved unless opt-in refresh; checks are advisory only. |
36
36
  | **MCP process package honesty (4.5.6; fail-closed in 4.6.4)** | Every MCP tool context → `processPackage` (`processPackageMismatch` / `processStale`, versions, `nextAction`). | Multi-checkout users: restart MCP after pin bump. From 4.6.4, stale process context is non-authoritative; `ark_identity` remains diagnostic and project tools return `PROCESS_PACKAGE_STALE` until restart/retarget. Prefer project-local CLI until identity matched and versions align. |
37
37
  | **Doctor design fitness** | `ark-check --doctor --json` → `doctor.designFitness`, `doctor.designSmells[]` | Additive. Stable smell `id`s: `io-under-application`, `handler-in-persistence`, `god-module`, `domain-logic-in-ui`, `facade-sql-in-routes`, `mixed-pattern-cluster`, `soft-contract`. `handler-in-persistence` covers static ES imports/re-exports of framework HTTP surfaces (`next/server`), `defineRoute` calls, and existing handler bodies inside Persistence-role layers or specific persistence paths; `require()` and dynamic `import()` are outside this narrow advisory, and a generic `Infrastructure` role alone is not Persistence. Persistence candidates are filtered and sorted before the bounded content scan so large application prefixes cannot hide the advisory. The detector inspects the first 800 sorted Persistence candidates; later candidates are uninspected, so **absence of a smell is not full-tree proof** above that envelope (incomplete/`partial` analysis also never proves “no smells”). **4.2 feedback hardening:** mode labels preserve the observed SUGGEST/ADAPT/ENFORCE state; a local permission/UI-state `canEdit` name alone is not a domain smell; real UI business rules route Domain → Application → UI; seed/fixture/demo/migration/generated files are not god-module pilots. Each smell has `evidence[]`, `fix`, technical `message`, and plain-language **`outcome`**. Does **not** fail the gate by itself. |
38
- | **Post-green Shape door** | `doctor.postGreenPath`, `doctor.primaryNextAction`, `doctor.healthyFinishedForbidden` | Additive when `designFitness.designWeak`. Single Shape door (`id: clarify-for-ai`): explore shape-focus → dual-plan B → autopilot only with OK. Never empty plan A = healthy finished. |
38
+ | **Post-green Shape door** | `doctor.postGreenPath`, `doctor.primaryNextAction`, `doctor.healthyFinishedForbidden` | Additive when `designFitness.designWeak`. Single Shape door (`id: clarify-for-ai`): explore shape-focus → dual-plan B → autopilot only with OK. Never empty plan A = healthy finished. A **passing** `ark-check` human run also prints one dim line naming `--plan`, the smell count and the smell ids. Its weakness test is `isDesignWeak` over the **blocking** violation count — the same input doctor uses, so the two surfaces cannot disagree about `designWeak`, and non-blocking type-only placement debt still gets the line. Report only: never a warning, never an exit-code change, silent when no smell remains, silent on `--changed` (a partial scan would report a slice as the tree) and on `--watch`. A baselined run is not called clean: the opening clause names the suppressed count instead. `--json` is unchanged. |
39
39
  | **Golden pattern (new code)** | Optional `.ark/golden-pattern.json`; doctor JSON `doctor.goldenPattern`; MCP `ark_place` / `ark_prepare_write` → `goldenPattern` | Additive, **advisory for NEW code only**. Required fields: `name`, `norm`; optional `newCodeHome`, `examplePath`, `schemaVersion`. **Absent is normal** (no claim). Never ENFORCE; never clears design-weak. Malformed → `invalid: true`, not silent guidance. |
40
40
  | **Plan pattern B (Shape bets)** | `ark-check --plan --json` → `plan.patternBets[]`, `plan.goal.designWeak` | Additive. Each bet: `id`, `smellId`, `pilot`, `evidence`, `successSignal`, `killSwitch`, **`neverMechanicalSafe: true`**, `class: "judgment"`. **Never** auto-applied by loop/autoPatch; not a `remediationKind` mechanical-safe. `goal.met` remains edge honesty only. |
41
41
  | **Pilot loop (one at a time)** | `plan.pilotLoop` / `doctor.pilotLoop` | Additive. When design-weak: `active`, `oneAtATime`, `neverMechanicalSafe`, **`nextPilot`** extraction-card fields (`pilotTarget`, `smellId`, `move`, `successSignal`, `killSwitch`, `doNot[]`). **One pilot → re-doctor**; never multi-pilot batch; never mechanical-safe. |
@@ -51,7 +51,7 @@ hardening guide remains repository-hosted rather than duplicated in the gate tar
51
51
  | **Report parity and snapshot evidence (4.2)** | `ark-check --report` → advisory sections (`data-advisory="contractHealth\|ambientState\|parseHealth\|arkRun"`, nested `governanceWeight`) + layer wall badges; `.ark/reports/*.json` | The report is a rendering of doctor truth. **Standing rule:** every doctor advisory ships with its report section — enforced by the `reportParity` guard, which enumerates the doctor's advisory keys and fails on any missing section. Snapshots add best-effort Git `HEAD`/branch/dirty provenance without a shell; unavailable Git is explicit. Evolution renders the Ark score delta only when both snapshots name the same ArkGate version, while retaining raw facts across versions. Thin `arkRun` on `latest.json` is `notAScore` residual honesty for `ark status`. |
52
52
  | **MCP project identity (4.2)** | `ark_identity`; `arkgate/schema/project-identity` or `arkgate/schema/ark.project-identity.schema.json`; root API constants/helpers/types | Schema `1.0`. `projectId` hashes canonical root + config path and stays stable across contract edits/restarts; runtime id/start time are separate. Every project-bound tool result and error carries `projectIdentity`, `binding` (`matched` / `unverified` / `mismatch`), and `authoritative`. Canonical out-of-root config/file evidence fails before project data. |
53
53
  | **MCP tools and compatibility resource** | `arkgate-mcp`; `ark_manifest`; `ark_status`; `ark://manifest` | Tool names and primary argument shapes are stable within a major. Every tool accepts additive `project.expectedRoot` / optional `expectedProjectId`. The initial handshake requires the exact project root; a contained descendant is authoritative only together with the matching project id. Legacy tool calls remain callable but `unverified` and non-authoritative. `ark_manifest` is the authoritative contract surface after binding. **`ark_status`** returns the status manifest envelope (parity with `ark status --json`). Standard `resources/read` cannot portably carry the expectation, so `ark://manifest` remains compatibility-only and always unverified/non-authoritative. The server never retargets from input. |
54
- | **`ark.config.json`** | Layer globs, rules, include/exclude, forbiddenGlobals, intent prefixes, `peerIsolation`, `dynamicImportAllowlist`, `safety` thresholds; optional **`arkRules`** map (schema `1.1+`); optional **`arkRun`** extra (schema `1.2+`); optional **`arkOrder`** extra (schema `1.3+`) | Versioned by `schemaVersion`; unknown fields fail closed and migrations preserve the previous supported major. Absence of `arkRules`, `arkRun`, or `arkOrder` is byte-for-byte silent on Layers / ArkRules verdicts. Enforced extra teeth share the CLI / MCP / hook / preflight / CI verdict and arm only when the layer plane is classified (same ArkRules floor). |
54
+ | **`ark.config.json`** | Layer globs, rules, include/exclude, forbiddenGlobals, intent prefixes, `peerIsolation`, `dynamicImportAllowlist`, `safety` thresholds; optional **`coverage`** controls (`testGlobs`, `maxFiles`); optional **`arkRules`** map (schema `1.1+`); optional **`arkRun`** extra (schema `1.2+`); optional **`arkOrder`** extra (schema `1.3+`) | Versioned by `schemaVersion`; unknown fields fail closed and migrations preserve the previous supported major. Absence of `coverage`, `arkRules`, `arkRun`, or `arkOrder` is byte-for-byte silent on Layers / ArkRules verdicts. Enforced extra teeth share the CLI / MCP / hook / preflight / CI verdict and arm only when the layer plane is classified (same ArkRules floor). |
55
55
  | **ArkRules inventory / under-contract (4.0; layer context 4.2)** | `ark-check --rules-inventory [--json]`; doctor `rulesUnderContract`; MCP `ark_rules_inventory` | Additive. Honest counts (inventoried / under-contract / frozen) — **never a score**. When configured layer evidence exists it overrides filename role guesses: a Domain file named `handler` is not a controller candidate. Test/fixture/seed/migration/exclusion surfaces plus narrow development-identity, PostgreSQL OID, and technical I/O constants are silent. Without layer evidence, backward-compatible path/content heuristics remain. Structure/invariant diagnostics use adapter `1.4` provenance. |
56
56
  | **`arkgate/schema/project-identity`** or **`arkgate/schema/ark.project-identity.schema.json`** | MCP canonical project, contract, runtime, expectation, and binding envelope | Schema `1.0`. Initial `expectedRoot` must be the exact project root. A contained descendant can match only when `expectedProjectId` is also present and correct; id-only matching stays non-authoritative. Mismatch codes are `PROJECT_ROOT_MISMATCH`, `PROJECT_ID_MISMATCH`, and `INVALID_PROJECT_EXPECTATION`. |
57
57
  | **Package pin dual-truth (4.0)** | doctor JSON `packageVersionTruth`; upgrade JSON/human note when pin behind CLI | Additive, advisory. Surfaces after `upgrade --no-install` when managed CLI is ahead of package.json. |
@@ -190,6 +190,36 @@ import { createOrderPlane } from 'arkgate/order';
190
190
 
191
191
  One install: `npm install arkgate`. `@arkgate/runtime` is deprecated.
192
192
 
193
+ ---
194
+
195
+ ## Installing from git
196
+
197
+ `npm install github:pedroknigge/arkgate` and `pnpm add git+https://github.com/pedroknigge/arkgate`
198
+ are supported for pinning an unreleased commit, and they need **no build-allowlist entry**:
199
+ this package declares no `prepack`, no `prepare`, and no install script. Through 4.8.3 the
200
+ `prepack` hook made a pnpm git install fail closed with
201
+ `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` — the whole install, not just the build — and the only
202
+ way through was adding `arkgate` to `allowBuilds` for a script that shells out to `npm`.
203
+ The publish path builds explicitly instead (`scripts/release-npm.mjs`, plus `prepublishOnly`
204
+ as a backstop, which pnpm does not run when it prepares a git dependency).
205
+
206
+ A git install is **not** the same package as the npm tarball, because `dist/` is a build
207
+ output and is not committed:
208
+
209
+ | From a git install | Works | Why |
210
+ |---|---|---|
211
+ | `arkgate` / `arkgate-check` (and `ark` / `ark-check`) | **yes** | `bin/` and `bin/lib/*.mjs` are committed sources — the deliberate zero-build CLI |
212
+ | `arkgate/schema*` subpath exports | **yes** | `schemas/*.json` are committed |
213
+ | `import … from 'arkgate'`, `arkgate/eslint`, `arkgate/order`, `arkgate/runtime`, `arkgate/nestjs` | **no** | all resolve into `dist/` |
214
+ | `ark-mcp` / `arkgate-mcp` | **no** | loads `dist/index.js`; it says so and names this section |
215
+
216
+ So: install from git when you want the **CLI or the CI check** at a specific commit, install
217
+ from npm (`npm i arkgate`) when you want the **library, the MCP server, or the ESLint plugin**.
218
+ To get everything from a checkout, clone and run `npm install && npm run build` — that is the
219
+ maintainer path in [develop.md](https://github.com/pedroknigge/arkgate/blob/main/docs/develop.md),
220
+ and the build stays *your* explicit command, never a script your package manager has to be
221
+ allowed to run.
222
+
193
223
  See [production-hardening.md](https://github.com/pedroknigge/arkgate/blob/main/docs/production-hardening.md) for requirements an eventual
194
224
  production deployment would need to satisfy; it is not a readiness certification.
195
225
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkgate",
3
- "version": "4.8.3",
3
+ "version": "4.8.4",
4
4
  "description": "When the agent writes a bad import, the write doesn’t land. The same check fails the pull request.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -163,7 +163,7 @@
163
163
  "audit:beta-exit": "node scripts/beta-exit-audit.mjs",
164
164
  "clean": "rm -rf dist",
165
165
  "release:npm": "node scripts/release-npm.mjs",
166
- "prepack": "npm run build",
166
+ "prepublishOnly": "npm run build",
167
167
  "bench:hook-path": "node scripts/hook-path-bench.mjs"
168
168
  },
169
169
  "dependencies": {
@@ -625,6 +625,9 @@
625
625
  "allowDisabledPeerIsolation": false
626
626
  }
627
627
  },
628
+ "coverage": {
629
+ "$ref": "#/$defs/coverage"
630
+ },
628
631
  "arkRules": {
629
632
  "type": "object",
630
633
  "additionalProperties": {
@@ -773,6 +776,37 @@
773
776
  },
774
777
  "uniqueItems": true,
775
778
  "minItems": 1
779
+ },
780
+ "sharedRoots": {
781
+ "type": "array",
782
+ "items": {
783
+ "type": "string",
784
+ "minLength": 1
785
+ },
786
+ "uniqueItems": true,
787
+ "minItems": 1
788
+ },
789
+ "allowedCrossSlice": {
790
+ "type": "array",
791
+ "minItems": 1,
792
+ "items": {
793
+ "type": "object",
794
+ "additionalProperties": false,
795
+ "required": [
796
+ "from",
797
+ "to"
798
+ ],
799
+ "properties": {
800
+ "from": {
801
+ "type": "string",
802
+ "minLength": 1
803
+ },
804
+ "to": {
805
+ "type": "string",
806
+ "minLength": 1
807
+ }
808
+ }
809
+ }
776
810
  }
777
811
  }
778
812
  },
@@ -800,6 +834,35 @@
800
834
  }
801
835
  }
802
836
  },
837
+ "coverage": {
838
+ "type": "object",
839
+ "additionalProperties": false,
840
+ "description": "Invariant coverage scan controls. testGlobs replaces the built-in test-name heuristic; maxFiles raises or lowers the evidence file budget; coverageRoots declares where the project runs its tests, so a covering test found outside them is reported instead of silently certifying an invariant.",
841
+ "properties": {
842
+ "testGlobs": {
843
+ "type": "array",
844
+ "items": {
845
+ "type": "string",
846
+ "minLength": 1
847
+ },
848
+ "uniqueItems": true,
849
+ "minItems": 1
850
+ },
851
+ "maxFiles": {
852
+ "type": "integer",
853
+ "minimum": 1
854
+ },
855
+ "coverageRoots": {
856
+ "type": "array",
857
+ "items": {
858
+ "type": "string",
859
+ "minLength": 1
860
+ },
861
+ "uniqueItems": true,
862
+ "minItems": 1
863
+ }
864
+ }
865
+ },
803
866
  "arkRun": {
804
867
  "type": "object",
805
868
  "additionalProperties": false,
package/server.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.pedroknigge/arkgate",
4
- "description": "When the agent writes a bad import, the write doesn’t land. The same check fails the pull request.",
4
+ "description": "When the agent writes a bad import, the write doesn\u2019t land. The same check fails the pull request.",
5
5
  "repository": {
6
6
  "url": "https://github.com/pedroknigge/arkgate",
7
7
  "source": "github"
8
8
  },
9
- "version": "4.8.3",
9
+ "version": "4.8.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "arkgate",
14
- "version": "4.8.3",
14
+ "version": "4.8.4",
15
15
  "runtimeHint": "npx",
16
16
  "transport": {
17
17
  "type": "stdio"