arkgate 4.8.2 → 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.
- package/CHANGELOG.md +257 -3
- package/README.md +47 -9
- package/bin/ark-check-runtime.mjs +340 -5
- package/bin/ark-layer-match.mjs +170 -13
- package/bin/ark-mcp-runtime.mjs +9 -2
- package/bin/lib/analysis-completeness.mjs +86 -0
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +2 -0
- package/bin/lib/ark-order-facts.mjs +59 -0
- package/bin/lib/ark-order-sensors.mjs +31 -2
- package/bin/lib/arkrule-file-hints.mjs +6 -2
- package/bin/lib/arkrules-contract.mjs +9 -1
- package/bin/lib/arkrules-sensors.mjs +22 -2
- package/bin/lib/check-args.mjs +66 -0
- package/bin/lib/config-contract.mjs +26 -0
- package/bin/lib/config-extras.mjs +2 -0
- package/bin/lib/design-smells.mjs +85 -0
- package/bin/lib/diagnostic-catalog.mjs +8 -2
- package/bin/lib/first-run-help.mjs +12 -0
- package/bin/lib/invariant-coverage-io.mjs +175 -19
- package/bin/lib/invariant-coverage.mjs +110 -7
- package/bin/lib/literal-path-drift-io.mjs +569 -0
- package/bin/lib/literal-path-drift.mjs +761 -0
- package/bin/lib/policy-delta-io.mjs +5 -0
- package/bin/lib/remediation.mjs +24 -1
- package/bin/lib/resolved-candidate-facts.mjs +31 -0
- package/bin/lib/rules-under-contract.mjs +5 -0
- package/bin/lib/scan-files.mjs +54 -0
- package/bin/lib/sensor-promote-cli.mjs +372 -0
- package/bin/lib/sensor-promote-io.mjs +246 -0
- package/bin/lib/sensor-promotion.mjs +363 -0
- package/dist/{configTypes-BdCe_gvv.d.ts → configTypes-dy5PfTqS.d.ts} +36 -0
- package/dist/{diagnosticCatalog-CPzH-MLN.d.ts → diagnosticCatalog-DgTs0abp.d.ts} +169 -11
- package/dist/eslint/index.cjs +5 -5
- package/dist/eslint/index.d.ts +34 -1
- package/dist/eslint/index.js +5 -5
- package/dist/index.cjs +31 -31
- package/dist/index.d.ts +85 -7
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +13 -13
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +13 -13
- package/dist/{types-DCSlrRnV.d.ts → types-BuM8WNqe.d.ts} +1 -1
- package/dist/{types-C9KApBzX.d.ts → types-D95drJ3_.d.ts} +1 -1
- package/docs/README.md +4 -4
- package/docs/agent-guide.md +182 -0
- package/docs/configuration.md +89 -9
- package/docs/develop.md +24 -2
- package/docs/diagnostics.md +79 -1
- package/docs/enthusiast/README.md +6 -4
- package/docs/package-surface.md +36 -4
- package/docs/product-voice.md +15 -5
- package/docs/use.md +8 -5
- package/package.json +2 -2
- package/schemas/ark.arkrules.schema.json +1 -0
- package/schemas/ark.config.schema.json +72 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/server.json +3 -3
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +13 -3
- package/templates/agent-skills/ark-autopilot/SKILL.md +1 -1
- package/templates/agent-skills/ark-contract/SKILL.md +4 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -0
- package/templates/agent-skills/ark-place/SKILL.md +6 -2
- package/templates/arkrules/ApplicationOrchestration.json +6 -0
- package/templates/skills/ark-adopt.md +13 -3
- package/templates/skills/ark-autopilot.md +1 -1
- package/templates/skills/ark-contract.md +4 -0
- package/templates/skills/ark-coverage.md +1 -0
- package/templates/skills/ark-place.md +6 -2
|
@@ -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-
|
|
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
|
@@ -45,7 +45,7 @@ Product site: [arkgate.online](https://www.arkgate.online/) · npm: [`arkgate`](
|
|
|
45
45
|
|-----|------------|
|
|
46
46
|
| [CONTRIBUTING.md](../CONTRIBUTING.md) | Setup, rules, PR/release |
|
|
47
47
|
| [ROADMAP.md](../ROADMAP.md) | Live implementation queue (one `doing` at a time). History: [archive/roadmap-history.md](archive/roadmap-history.md) |
|
|
48
|
-
| [adr/](adr/README.md) | Architecture decisions |
|
|
48
|
+
| [adr/](adr/README.md) | Architecture decisions ([0032](adr/0032-writes-via-aggregate-sensor.md) writes-via-aggregate) |
|
|
49
49
|
| [SECURITY.md](../SECURITY.md) · [threat-model.md](threat-model.md) | Security |
|
|
50
50
|
|
|
51
51
|
---
|
|
@@ -62,8 +62,8 @@ 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.
|
|
66
|
-
Prior: [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).
|
|
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
|
+
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
|
|
|
69
69
|
---
|
|
@@ -74,4 +74,4 @@ Older notes: [releases/](releases/). Config: [configuration.md](configuration.md
|
|
|
74
74
|
2. **One primary flow** — `start` → doctor → optional guided work.
|
|
75
75
|
3. **Honest hardness** — host write guarantees differ; a **required GitHub status context** running the merge CLI is the shared hard boundary.
|
|
76
76
|
4. **History is not the product** — version archaeology lives under `releases/` and `plans/`, not the front door.
|
|
77
|
-
5. **Common language** — first-contact copy uses ordinary software words (import rules, the write doesn’t land, required CI). ArkGate is import rules; ArkRules is policies; ArkRun is an experimental runtime; ArkOrder
|
|
77
|
+
5. **Common language** — first-contact copy uses ordinary software words (import rules, the write doesn’t land, required CI). ArkGate is import rules; ArkRules is policies; ArkRun is an experimental runtime; ArkOrder stops the agent rewriting the few slow product decisions as CRUD. Voice: [product-voice.md](product-voice.md).
|
package/docs/agent-guide.md
CHANGED
|
@@ -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
|
|
package/docs/configuration.md
CHANGED
|
@@ -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**.
|
|
@@ -99,10 +126,12 @@ Top-level fields:
|
|
|
99
126
|
preflight / CI verdict and arm only when the layer plane is classified (same ≥50%
|
|
100
127
|
governed and ≥1 populated-layer floor as ArkRules).
|
|
101
128
|
- **`arkOrder`** (optional, schema `1.3+`) — inline ArkOrder extra (`mode`, `planeRoots`,
|
|
102
|
-
`managedLayers`, `maxXiKeys
|
|
129
|
+
`managedLayers`, `maxXiKeys`, **`xiKeys`**). Absence is silent. Unknown keys fail closed.
|
|
103
130
|
Import `createOrderPlane` from `arkgate/order` (same package). Empty `planeRoots` in
|
|
104
|
-
`enforced` mode fails closed (`ARKORDER_MISSING_PLANE`).
|
|
105
|
-
|
|
131
|
+
`enforced` mode fails closed (`ARKORDER_MISSING_PLANE`). `xiKeys` are the 3–5 slow
|
|
132
|
+
names the product already knows (plan, protocol, cost-code bound). Empty `xiKeys`
|
|
133
|
+
leaves `ARKORDER_XI_FIELD_WRITE` silent. Membership ids are not keys. Demotion or
|
|
134
|
+
deletion is a policy-delta **weakening**. Field ingest never mints a pattern.
|
|
106
135
|
|
|
107
136
|
Layer fields:
|
|
108
137
|
|
|
@@ -128,11 +157,60 @@ process module-capability family must be denied.
|
|
|
128
157
|
|
|
129
158
|
Rule fields:
|
|
130
159
|
|
|
131
|
-
- `from`, `to`, `allowed`, `message`, `peerIsolation`, `sliceFolders`
|
|
160
|
+
- `from`, `to`, `allowed`, `message`, `peerIsolation`, `sliceFolders`, `sharedRoots`,
|
|
161
|
+
`allowedCrossSlice`
|
|
132
162
|
- `peerIsolation: true` + `allowed: false`: deny only when slice ids differ; same-slice allows
|
|
133
163
|
when both paths classify. Missing paths, empty slice folders, or unclassifiable slices
|
|
134
164
|
**fail closed** (deny — cannot prove same-slice).
|
|
135
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
|
+
|
|
136
214
|
### Type-only edges (placement debt)
|
|
137
215
|
|
|
138
216
|
`import type` / `export type` and pure type-only named bindings are **type placement debt**, not
|
|
@@ -161,15 +239,17 @@ See [brownfield adoption](brownfield-adoption.md#nextjs-honesty-default-overlays
|
|
|
161
239
|
| **Structure sensors** | Intra-layer heuristics | Only `mode: "enforced"` |
|
|
162
240
|
| **Invariants** | Catalog + coverage evidence (not a business runtime) | Only enforced + proven-uncovered |
|
|
163
241
|
| **ArkRun** (opt-in extra) | Kernel usage + complete declarations | Only `arkRun.mode: "enforced"` when classified |
|
|
242
|
+
| **ArkOrder** (opt-in extra) | Frozen pattern + four verbs (no `update`) | Only `arkOrder.mode: "enforced"` when classified |
|
|
164
243
|
|
|
165
|
-
Absence of `arkRules` or `
|
|
166
|
-
invariants, and advisory
|
|
167
|
-
`arkrules/*` file does not make merge fail structure alone. Enforced structure/invariants/ArkRun arm
|
|
244
|
+
Absence of `arkRules`, `arkRun`, or `arkOrder` adds **no** extra merge teeth. **Advisory** structure sensors, advisory
|
|
245
|
+
invariants, advisory ArkRun, and advisory ArkOrder also add **no** merge teeth (FG-ARKRULES-ADVISORY-ONLY / ADR 0020 / ADR 0027) — packing every starter
|
|
246
|
+
`arkrules/*` file does not make merge fail structure alone. Enforced structure/invariants/ArkRun/ArkOrder arm
|
|
168
247
|
`mergePlanes.extraMergeTeeth` only when the layer plane is honestly classified
|
|
169
248
|
(governed ≥ 50% and ≥ 1 populated layer); empty classification never gets extra-plane teeth
|
|
170
249
|
(P1M-EXTRATEETH-EMPTY-GRAPH). Extra planes **never** merge into one architecture
|
|
171
250
|
score. Doctor exposes `rulesUnderContract.mergePlanes` (including `mergePlanes.arkRun`) for which plane can fail,
|
|
172
|
-
and a dedicated `doctor.arkRun` section that is always `notAScore`.
|
|
251
|
+
and a dedicated `doctor.arkRun` section that is always `notAScore`. ArkOrder skip findings are
|
|
252
|
+
`ARKORDER_*` diagnostics on that same extra-teeth floor.
|
|
173
253
|
|
|
174
254
|
Safety fields:
|
|
175
255
|
|
|
@@ -196,7 +276,7 @@ Each `arkrules/<Layer>.json` may declare:
|
|
|
196
276
|
|
|
197
277
|
| Section | Purpose | Modes | What it really enforces |
|
|
198
278
|
|---------|---------|--------|-------------------------|
|
|
199
|
-
| `structure[]` | Closed sensor ids (e.g. `orchestration-only`, `thin-adapter`, `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. Tier-2 sensors (`no-anemic-model`) stay advisory-only (cannot promote to enforced). |
|
|
279
|
+
| `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). |
|
|
200
280
|
| `invariants[]` | Stable ids + description + `coverage` (`test` / `symbol`) + optional `appliesTo` globs | `advisory` or `enforced` | **Named policy + evidence** (symbol in source and/or test title/content). Does **not** execute business logic at check time and does **not** replace behavior/property tests. |
|
|
201
281
|
|
|
202
282
|
**Reporting:** diagnostics carry `evidence.arkruleId` + `evidence.arkruleSource`. Label residual
|
package/docs/develop.md
CHANGED
|
@@ -6,8 +6,9 @@ For **developers** integrating ArkGate into a product repo: agents, CI, config,
|
|
|
6
6
|
|
|
7
7
|
When the agent writes a bad import, the write doesn’t land. The same check fails the pull
|
|
8
8
|
request. Not an API Gateway. Not a folder linter. Without a required CI status,
|
|
9
|
-
`ark.config.json` is just documentation. ArkGate is import rules. ArkRules
|
|
10
|
-
are optional — policies
|
|
9
|
+
`ark.config.json` is just documentation. ArkGate is import rules. ArkRules, ArkRun,
|
|
10
|
+
and ArkOrder are optional — policies, an experimental runtime, and the extra that
|
|
11
|
+
stops slow product decisions being CRUD. Not a second install.
|
|
11
12
|
|
|
12
13
|
If you only want the happy path, start at [use.md](use.md).
|
|
13
14
|
|
|
@@ -85,6 +86,7 @@ can show `runtimeObserved: true` even when branch-protection policy is plan-unav
|
|
|
85
86
|
| Layers, rules, globs | [configuration.md](configuration.md) · `ark.config.json` |
|
|
86
87
|
| ArkRules (structure + invariants) | [configuration.md](configuration.md#arkrules-intra-layer-opt-in) · `arkRules` / `arkrules/*` · skill `/ark-adopt` (session 0) or `/ark-autopilot` |
|
|
87
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) |
|
|
88
90
|
| Stable package API | [package-surface.md](package-surface.md) |
|
|
89
91
|
| Diagnostic codes (`ruleId` why/fix) | [diagnostics.md](diagnostics.md) · root `DIAGNOSTIC_CATALOG` |
|
|
90
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` |
|
|
@@ -186,6 +188,26 @@ See [configuration.md](configuration.md), [package-surface.md](package-surface.m
|
|
|
186
188
|
|
|
187
189
|
---
|
|
188
190
|
|
|
191
|
+
## Optional ArkOrder extra and plane
|
|
192
|
+
|
|
193
|
+
Layers can be green while the agent still PATCHes `plan` as if it were `seatCount`.
|
|
194
|
+
Optional **`arkOrder`** on `ark.config.json` (schema `1.3`) is the extra that makes
|
|
195
|
+
those few slow product decisions a write rule: field events absorb or escalate;
|
|
196
|
+
a generic `update` of the plan does not land. Absence is silent. Compact starters
|
|
197
|
+
leave it off.
|
|
198
|
+
|
|
199
|
+
The plane is `arkgate/order` in package `arkgate`, not a second install.
|
|
200
|
+
`createOrderPlane` is the factory. Call it only in `arkOrder.planeRoots`. Domain
|
|
201
|
+
stays plane-free. Empty `planeRoots` in `enforced` mode fails closed
|
|
202
|
+
(`ARKORDER_MISSING_PLANE`). In-memory. Not durable. Does not replace ArkRun.
|
|
203
|
+
|
|
204
|
+
Copy [examples/arkorder-billing/](../examples/arkorder-billing/) (`plan` / `cycle` /
|
|
205
|
+
`tenancy`) and rename the keys. Posting an invoice is absorbed; changing plan is a
|
|
206
|
+
new release. See [configuration.md](configuration.md) and
|
|
207
|
+
[package-surface.md](package-surface.md).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
189
211
|
## Migrate from `ark-runtime-kernel`
|
|
190
212
|
|
|
191
213
|
Same product, new package name: [migrate-from-ark-runtime-kernel.md](migrate-from-ark-runtime-kernel.md).
|
package/docs/diagnostics.md
CHANGED
|
@@ -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
|
|
|
@@ -400,6 +415,15 @@ Haken slaving: few slow keys (ξ) determine derived fast state. Field ingest nev
|
|
|
400
415
|
- **Why:** An ingest() result is written into a Release or ξ store. ingest may absorb or escalate; it never mints a pattern.
|
|
401
416
|
- **Fix:** Keep ingest results as absorb/escalate only. Change ξ with proposeRelease + release. Never mechanical-safe.
|
|
402
417
|
|
|
418
|
+
<a id="ARKORDER_XI_FIELD_WRITE"></a>
|
|
419
|
+
|
|
420
|
+
### `ARKORDER_XI_FIELD_WRITE`
|
|
421
|
+
|
|
422
|
+
**Slow key written around the order plane**
|
|
423
|
+
|
|
424
|
+
- **Why:** A managed-layer file imports a persistence driver and writes a declared arkOrder.xiKeys name. Field events absorb or escalate; they do not PATCH the slow pattern.
|
|
425
|
+
- **Fix:** Keep invoices, seats, hours, and logs on ingest. Change the slow key with proposeRelease + release, then preflight again. Never mechanical-safe.
|
|
426
|
+
|
|
403
427
|
## Atomic preflight and change sets
|
|
404
428
|
|
|
405
429
|
<a id="INVALID_CHANGE_PATH"></a>
|
|
@@ -521,6 +545,15 @@ Haken slaving: few slow keys (ξ) determine derived fast state. Field ingest nev
|
|
|
521
545
|
- **Why:** Single-file validation cannot prove project module resolution. The write hook is already the verdict.
|
|
522
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.
|
|
523
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
|
+
|
|
524
557
|
<a id="ANALYSIS_HOST_UNAVAILABLE"></a>
|
|
525
558
|
|
|
526
559
|
### `ANALYSIS_HOST_UNAVAILABLE`
|
|
@@ -530,6 +563,51 @@ Haken slaving: few slow keys (ξ) determine derived fast state. Field ingest nev
|
|
|
530
563
|
- **Why:** No usable TypeScript / analysis host was available for this invocation.
|
|
531
564
|
- **Fix:** Install a supported TypeScript version visible to the project, then re-run. Unavailable analysis is fail-closed.
|
|
532
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
|
+
|
|
533
611
|
## Port adapters
|
|
534
612
|
|
|
535
613
|
<a id="ADAPTER_NOT_ALLOWED_FOR_PORT"></a>
|
|
@@ -25,16 +25,18 @@ aliases `ark` / `ark-check` still work).
|
|
|
25
25
|
|
|
26
26
|
`ark start` and `ark-check --plan` will not claim "everything is guarded" while governed coverage is near zero. On Nest/Next/express starters, init also merges **framework filename conventions** into the layer globs so day-one coverage is real.
|
|
27
27
|
|
|
28
|
-
###
|
|
28
|
+
### Layers always. Extras when you ask.
|
|
29
29
|
|
|
30
30
|
| Kind | Plain English |
|
|
31
31
|
|------|---------------|
|
|
32
32
|
| **Layers** | Who may import whom (always on after start) |
|
|
33
33
|
| **ArkRules** (optional) | Habits *inside* a layer — structure and named invariants as data |
|
|
34
|
-
| **ArkRun** (optional extra) |
|
|
34
|
+
| **ArkRun** (optional extra) | How the app talks (`arkgate/runtime`; `arkRun` on schema `1.2`) |
|
|
35
|
+
| **ArkOrder** (optional extra) | Stops the agent rewriting the few slow product decisions as CRUD (`arkgate/order`; `arkOrder` on schema `1.3`) |
|
|
35
36
|
|
|
36
|
-
You can stay on layers only. When you add
|
|
37
|
-
coverage. Compact starters leave ArkRun off. Residual labels: **`[Layer]`** vs
|
|
37
|
+
You can stay on layers only. When you add an extra, start **advisory** and promote only with
|
|
38
|
+
coverage. Compact starters leave ArkRun and ArkOrder off. Residual labels: **`[Layer]`** vs
|
|
39
|
+
**`[ArkRules]`** vs **`[ArkRun]`** vs **`[ArkOrder]`**. Details:
|
|
38
40
|
[use.md](../use.md) · [configuration](../configuration.md).
|
|
39
41
|
|
|
40
42
|
## Start here
|