arkgate 4.8.18 → 4.8.20
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 +153 -0
- package/README.md +5 -6
- package/bin/ark-check-runtime.mjs +23 -0
- package/bin/ark-layer-match.mjs +141 -9
- package/bin/ark-mcp-runtime.mjs +3 -3
- package/bin/ark-shared.mjs +187 -16
- package/bin/ark.mjs +23 -19
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/ci-and-commands.mjs +2 -2
- package/bin/lib/config-contract.mjs +1 -0
- package/bin/lib/doctor-advisories.mjs +2 -1
- package/bin/lib/doctor-human.mjs +2 -0
- package/bin/lib/doctor-next-actions.mjs +35 -0
- package/bin/lib/doctor-plan.mjs +8 -8
- package/bin/lib/enforcement-honesty.mjs +30 -2
- package/bin/lib/gate-files.mjs +22 -1
- package/bin/lib/host-support-matrix.mjs +1 -1
- package/bin/lib/html-report-depth.mjs +6 -2
- package/bin/lib/install-migrate.mjs +3 -4
- package/bin/lib/invariant-coverage.mjs +44 -13
- package/bin/lib/managed-upgrade.mjs +30 -5
- package/bin/lib/mcp-adoption.mjs +4 -1
- package/bin/lib/package-manager.mjs +5 -2
- package/bin/lib/physical-cohesion.mjs +68 -51
- package/bin/lib/presets.mjs +14 -14
- package/bin/lib/reshape-decisions.mjs +2 -2
- package/bin/lib/rules-under-contract.mjs +40 -4
- package/bin/lib/scan-files.mjs +3 -26
- package/bin/lib/skill-install.mjs +3 -2
- package/bin/lib/start-install-recovery.mjs +156 -0
- package/bin/lib/start-preview.mjs +17 -16
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/{configTypes-VD0qcubY.d.ts → configTypes-VVknqcTJ.d.ts} +2 -0
- package/dist/{diagnosticCatalog-BNxKdcN4.d.ts → diagnosticCatalog-BPYWNyJR.d.ts} +9 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +12 -4
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +18 -18
- package/dist/index.d.ts +8 -4
- package/dist/index.js +20 -20
- 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 +10 -10
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +10 -10
- package/dist/{types-BSzRy2X1.d.ts → types-Cs-46DRu.d.ts} +1 -1
- package/dist/{types-D5GT5ZT8.d.ts → types-p_fCp6P-.d.ts} +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +3 -1
- package/docs/ai-gates.md +7 -3
- package/docs/configuration.md +20 -5
- package/docs/develop.md +1 -2
- package/docs/package-surface.md +2 -2
- package/docs/use.md +1 -2
- package/package.json +3 -3
- package/schemas/ark.config.schema.json +6 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +15 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +2 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
- package/templates/agent-skills/ark-explain/SKILL.md +1 -1
- package/templates/agent-skills/ark-explore/SKILL.md +13 -1
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +5 -3
- package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
- package/templates/skills/ark-adopt.md +15 -1
- package/templates/skills/ark-autopilot.md +2 -0
- package/templates/skills/ark-coverage.md +1 -1
- package/templates/skills/ark-explain.md +1 -1
- package/templates/skills/ark-explore.md +13 -1
- package/templates/skills/ark-place.md +2 -0
- package/templates/skills/ark-upgrade.md +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -96,6 +96,159 @@ in the immutable pre-2.0 archive linked below.
|
|
|
96
96
|
Existing configs keep whatever URL they already have. Editor completion
|
|
97
97
|
matches the 4.x line you just installed.
|
|
98
98
|
|
|
99
|
+
## 4.8.20 — 2026-09-25
|
|
100
|
+
|
|
101
|
+
**Patch** over **4.8.19**. Daily accumulate (2026-09-25 ART) of the ships
|
|
102
|
+
that landed after 4.8.19 published: `coverage.symbol` is a non-test
|
|
103
|
+
declaration
|
|
104
|
+
([#301](https://github.com/pedroknigge/arkgate/pull/301) /
|
|
105
|
+
[#291](https://github.com/pedroknigge/arkgate/issues/291)), reshape pilot
|
|
106
|
+
stays in the source layer and slice
|
|
107
|
+
([#299](https://github.com/pedroknigge/arkgate/pull/299) /
|
|
108
|
+
[#295](https://github.com/pedroknigge/arkgate/issues/295)),
|
|
109
|
+
physical-cohesion residual clears finished and elegant
|
|
110
|
+
([#302](https://github.com/pedroknigge/arkgate/pull/302) /
|
|
111
|
+
[#296](https://github.com/pedroknigge/arkgate/issues/296)), anchored
|
|
112
|
+
starred `sliceFolders`
|
|
113
|
+
([#300](https://github.com/pedroknigge/arkgate/pull/300) /
|
|
114
|
+
[#294](https://github.com/pedroknigge/arkgate/issues/294)), shared roots
|
|
115
|
+
cannot import a slice when opted in
|
|
116
|
+
([#303](https://github.com/pedroknigge/arkgate/pull/303) /
|
|
117
|
+
[#297](https://github.com/pedroknigge/arkgate/issues/297)), pnpm Age
|
|
118
|
+
install-fail recovery
|
|
119
|
+
([#293](https://github.com/pedroknigge/arkgate/pull/293) /
|
|
120
|
+
[#292](https://github.com/pedroknigge/arkgate/issues/292)), vitest 4.1.11
|
|
121
|
+
([#298](https://github.com/pedroknigge/arkgate/pull/298)), Dependabot
|
|
122
|
+
vitest 5 ignore
|
|
123
|
+
([#304](https://github.com/pedroknigge/arkgate/pull/304)), and docs
|
|
124
|
+
([#244](https://github.com/pedroknigge/arkgate/pull/244),
|
|
125
|
+
[#234](https://github.com/pedroknigge/arkgate/pull/234)).
|
|
126
|
+
**Write. Check. Ship.** **No required config migration.** No
|
|
127
|
+
`schemaVersion` bump. Does not close `K01` / `Z09`. This mother
|
|
128
|
+
`ark.config.json` still does **not** turn `arkOrder` on.
|
|
129
|
+
|
|
130
|
+
**Status: published** (npm `latest` is **4.8.20**).
|
|
131
|
+
|
|
132
|
+
### Changed
|
|
133
|
+
- This is a behavior change. Some repos that are green today will turn red
|
|
134
|
+
when `coverage.symbol` only appeared in tests or imports.
|
|
135
|
+
`symbolEvidenceFile` is the non-test file that declares the symbol.
|
|
136
|
+
Discard counters (`depthLimited`, the per-file byte cap, and the other
|
|
137
|
+
existing reasons) are printed on green doctor and `--rules-inventory`
|
|
138
|
+
output too; the depth and byte caps themselves stay internal
|
|
139
|
+
([#301](https://github.com/pedroknigge/arkgate/pull/301) /
|
|
140
|
+
[#291](https://github.com/pedroknigge/arkgate/issues/291)).
|
|
141
|
+
- `sliceFolders` accepts a starred prefix (`lib/features/*/*`), anchored
|
|
142
|
+
like `sharedRoots`. The slice id is the directories the stars bind
|
|
143
|
+
(`lib/features/projects/rfi`). A star never binds a filename; a file
|
|
144
|
+
directly under that prefix stays in the last directory
|
|
145
|
+
(`lib/features/projects`). Bare `sliceFolders` names no longer treat a
|
|
146
|
+
filename as the slice child segment.
|
|
147
|
+
`src/components/features/projects/rfi-status-pill.tsx` with `["projects"]`
|
|
148
|
+
is not a slice. Bare names under `src/lib/features` behave exactly as
|
|
149
|
+
before: `src/lib/features/projects/rfi/x.ts` with `["features"]` stays
|
|
150
|
+
`features/projects` (a regression test covers it)
|
|
151
|
+
([#300](https://github.com/pedroknigge/arkgate/pull/300) /
|
|
152
|
+
[#294](https://github.com/pedroknigge/arkgate/issues/294)).
|
|
153
|
+
|
|
154
|
+
### Fixed
|
|
155
|
+
- Reshape pilot destinations stay in the source file's layer and slice. A
|
|
156
|
+
proposal that would leave both is withheld instead of pointing at
|
|
157
|
+
`src/features/`
|
|
158
|
+
([#299](https://github.com/pedroknigge/arkgate/pull/299) /
|
|
159
|
+
[#295](https://github.com/pedroknigge/arkgate/issues/295)).
|
|
160
|
+
- Doctor no longer calls the tree finished or elegant while a
|
|
161
|
+
physical-cohesion finding is still unsuppressed. The gate verdict is
|
|
162
|
+
unchanged, and the reshape card stays one judgment
|
|
163
|
+
([#302](https://github.com/pedroknigge/arkgate/pull/302) /
|
|
164
|
+
[#296](https://github.com/pedroknigge/arkgate/issues/296)).
|
|
165
|
+
- A `peerIsolation` rule may set `sharedImportsSlice` to `"deny"`. A
|
|
166
|
+
declared shared root then may not import a slice
|
|
167
|
+
(`shared root … → slice …`). The default stays off, so current walls
|
|
168
|
+
stay green. A slice importing a shared root stays allowed.
|
|
169
|
+
`allowedCrossSlice` does not excuse this hop. Doctor lists shared-root
|
|
170
|
+
→ slice bridges even when the flag is off. A green run still says the
|
|
171
|
+
wall is direct-only
|
|
172
|
+
([#303](https://github.com/pedroknigge/arkgate/pull/303) /
|
|
173
|
+
[#297](https://github.com/pedroknigge/arkgate/issues/297)).
|
|
174
|
+
- Age-gated `start --apply` install failure no longer reprints the same
|
|
175
|
+
`pnpm add` as the primary next step. Recovery names pnpm Age, leads
|
|
176
|
+
with `npx --package=arkgate@<pin> arkgate-check --doctor`, and says
|
|
177
|
+
`minimumReleaseAgeExclude` must live in pnpm config or
|
|
178
|
+
`pnpm-workspace.yaml` (a CLI flag is not enough)
|
|
179
|
+
([#293](https://github.com/pedroknigge/arkgate/pull/293) /
|
|
180
|
+
[#292](https://github.com/pedroknigge/arkgate/issues/292)).
|
|
181
|
+
|
|
182
|
+
### Dependencies
|
|
183
|
+
- Dev dependency `vitest` and `@vitest/coverage-v8` are on 4.1.11, which
|
|
184
|
+
pulls `@vitest/mocker@4.1.11` and closes
|
|
185
|
+
[GHSA-82fw-gwwq-j7x9](https://github.com/vitest-dev/vitest/security/advisories/GHSA-82fw-gwwq-j7x9)
|
|
186
|
+
(moderate path traversal via redirect mocks). Vitest 4's V8 coverage
|
|
187
|
+
provider remaps with AST analysis, so the same suite reports a larger
|
|
188
|
+
branch denominator; coverage floors moved to just under those measured
|
|
189
|
+
numbers. No tests were removed
|
|
190
|
+
([#298](https://github.com/pedroknigge/arkgate/pull/298)).
|
|
191
|
+
- Dependabot ignores vitest 5 (`vitest`, `@vitest/coverage-v8`, and
|
|
192
|
+
`@vitest/mocker` at `>= 5.0.0`) until Node 22.12+ is the floor.
|
|
193
|
+
Version-update pull requests stay off; the cooldown is 7 days
|
|
194
|
+
([#304](https://github.com/pedroknigge/arkgate/pull/304)).
|
|
195
|
+
- Docs: anonymized live dogfood sales stats
|
|
196
|
+
([#244](https://github.com/pedroknigge/arkgate/pull/244)); `UC01` marked
|
|
197
|
+
done after [#233](https://github.com/pedroknigge/arkgate/pull/233)
|
|
198
|
+
([#234](https://github.com/pedroknigge/arkgate/pull/234)).
|
|
199
|
+
|
|
200
|
+
## 4.8.19 — 2026-09-20
|
|
201
|
+
|
|
202
|
+
**Patch** over **4.8.18**. Daily accumulate (2026-09-20 ART) of the five
|
|
203
|
+
ships that landed after 4.8.18 published: pre-hook write gate first
|
|
204
|
+
([#280](https://github.com/pedroknigge/arkgate/pull/280)), honest
|
|
205
|
+
workspace evidence + playground exclude
|
|
206
|
+
([#286](https://github.com/pedroknigge/arkgate/pull/286)), skill stamp
|
|
207
|
+
refresh ([#285](https://github.com/pedroknigge/arkgate/pull/285)),
|
|
208
|
+
honest start docs and install-fail next step
|
|
209
|
+
([#287](https://github.com/pedroknigge/arkgate/pull/287)), and honest
|
|
210
|
+
recommend label when `uiHeavy` is true
|
|
211
|
+
([#289](https://github.com/pedroknigge/arkgate/pull/289)).
|
|
212
|
+
**Write. Check. Ship.** **No required config migration.** No
|
|
213
|
+
`schemaVersion` bump. Does not close `K01` / `Z09`. This mother
|
|
214
|
+
`ark.config.json` still does **not** turn `arkOrder` on.
|
|
215
|
+
|
|
216
|
+
**Status: published** (npm `latest` is **4.8.19**).
|
|
217
|
+
|
|
218
|
+
### Changed
|
|
219
|
+
- Write-gate order: install and trust the host pre-hook first. MCP
|
|
220
|
+
`ark_prepare_write` is fallback when that path is missing or fail-open.
|
|
221
|
+
Compact Cursor now requires the fail-closed hook (MCP alone is not
|
|
222
|
+
enough). Doctor / start / host matrix name the fallback
|
|
223
|
+
([#280](https://github.com/pedroknigge/arkgate/pull/280)).
|
|
224
|
+
|
|
225
|
+
### Fixed
|
|
226
|
+
- Stranger-path `--recommend` treats `pnpm-workspace.yaml` (even without
|
|
227
|
+
`package.json#workspaces`) as workspace evidence, and no longer says
|
|
228
|
+
"not a workspace monorepo" on a workspace tree. Start / recommend skip
|
|
229
|
+
playground, fixture, and scaffold trees the same way the check walk
|
|
230
|
+
does ([#286](https://github.com/pedroknigge/arkgate/pull/286)).
|
|
231
|
+
- Managed upgrade refreshes a skill's `arkVersion` / `arkgate@X.Y.Z`
|
|
232
|
+
description stamp when the body is unchanged (`state: current`).
|
|
233
|
+
Frozen redirect stubs no longer keep a stale picker version after a
|
|
234
|
+
pin bump. Reuses `planSkillInstall` `stamp-refresh`;
|
|
235
|
+
`summary.metadataRefresh` counts those writes
|
|
236
|
+
([#285](https://github.com/pedroknigge/arkgate/pull/285) /
|
|
237
|
+
[#284](https://github.com/pedroknigge/arkgate/issues/284)).
|
|
238
|
+
- Stranger start path no longer leads with `npm install -D arkgate`
|
|
239
|
+
(hard-fail on `workspace:*`). Docs lead with `npx arkgate start`;
|
|
240
|
+
`start --apply` already pins with `pnpm add -w` / `yarn add -W`.
|
|
241
|
+
pnpm `minimumReleaseAge` recovery is a one-liner. Doctor after a
|
|
242
|
+
failed install leads with resolve the package, not "make CI required"
|
|
243
|
+
([#287](https://github.com/pedroknigge/arkgate/pull/287) /
|
|
244
|
+
[#268](https://github.com/pedroknigge/arkgate/issues/268)).
|
|
245
|
+
- `start` / `--recommend` no longer labels a UI monorepo as "API server
|
|
246
|
+
without UI in this repository" when `uiHeavy` is true. The one-minute
|
|
247
|
+
label becomes "API-heavy monorepo with UI packages". Default monorepo
|
|
248
|
+
start parks `web/**` / `web/src/**` on PresentationAdapters
|
|
249
|
+
([#289](https://github.com/pedroknigge/arkgate/pull/289) /
|
|
250
|
+
[#288](https://github.com/pedroknigge/arkgate/issues/288)).
|
|
251
|
+
|
|
99
252
|
## 4.8.18 — 2026-09-18
|
|
100
253
|
|
|
101
254
|
**Patch** over **4.8.17**. Daily accumulate (2026-09-18 ART) of the six
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Works with Cursor, Claude, Codex, and Grok.
|
|
|
39
39
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
|
-
> **ArkGate 4.8.
|
|
42
|
+
> **ArkGate 4.8.20** is on npm `latest`.
|
|
43
43
|
> Write. Check. Ship. Adopted = required GitHub
|
|
44
44
|
> status running `arkgate-check --strict-merge`, or an explicit `advisory-only` stance.
|
|
45
45
|
> Status is compact (`arkgate-check --doctor`; `--all` for Details). Optional **ArkRun**
|
|
@@ -47,7 +47,7 @@ Works with Cursor, Claude, Codex, and Grok.
|
|
|
47
47
|
> (`arkgate/order`) stops rewriting a big product choice — like the billing plan —
|
|
48
48
|
> as if it were a seat count. Change those choices through a valve, not a generic update.
|
|
49
49
|
> `@arkgate/runtime` is deprecated.
|
|
50
|
-
> [4.8.
|
|
50
|
+
> [4.8.20](CHANGELOG.md) · [4.8.11](docs/releases/4.8.11.md) · [4.8.10](docs/releases/4.8.10.md) · [4.8.9](docs/releases/4.8.9.md) · [4.8.8](docs/releases/4.8.8.md) · [4.8.7](docs/releases/4.8.7.md) · [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -66,14 +66,13 @@ Full map: **[docs/README.md](docs/README.md)**
|
|
|
66
66
|
## Start in one minute
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
npm install -D arkgate typescript
|
|
70
69
|
npx arkgate start # preview files + commands
|
|
71
70
|
npx arkgate start --apply # compact config + host router + CI plan
|
|
72
71
|
npx --package=arkgate arkgate-check --doctor # status — one next step
|
|
73
72
|
npx --package=arkgate arkgate-check --doctor --all # full details
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
`arkgate-check` is a command in the `arkgate` package, not its own npm package.
|
|
75
|
+
`start --apply` pins arkgate with this repo's package manager (`pnpm add -w` / `yarn add -W` at a workspace root). Do not run `npm install -D arkgate` at a `workspace:*` root — npm rejects that protocol. After a local install, `npx arkgate-check --doctor` also works. `arkgate-check` is a command in the `arkgate` package, not its own npm package. If pnpm Age (`minimumReleaseAge`) blocks a brand-new release, keep using `npx --package=arkgate@<version>` — do not re-run the same add. Put arkgate on `minimumReleaseAgeExclude` in pnpm config or `pnpm-workspace.yaml` (a CLI flag is not enough), or wait until the package is mature.
|
|
77
76
|
|
|
78
77
|
`start --apply` refuses when projected governed coverage is below 50% or
|
|
79
78
|
shape confidence is weak (below 0.6 with coverage under 80%), or when the
|
|
@@ -204,7 +203,7 @@ Codex hard write covers only a complete local `apply_patch`; Cursor covers only
|
|
|
204
203
|
installed + trusted, while shell/direct
|
|
205
204
|
filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
|
|
206
205
|
|
|
207
|
-
This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run `arkgate-check --doctor` (or `ark-check --doctor`) for the evidence actually detected in the current repository.
|
|
206
|
+
This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. **Write-gate order:** install and trust the host pre-hook first. MCP prepare is fallback when that path is missing or fail-open. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run `arkgate-check --doctor` (or `ark-check --doctor`) for the evidence actually detected in the current repository.
|
|
208
207
|
<!-- arkgate-host-support:end -->
|
|
209
208
|
|
|
210
209
|
#### Why required CI is the hard line
|
|
@@ -347,7 +346,7 @@ Compact starters leave the extra off. Details: [ArkOrder](docs/arkorder.md).
|
|
|
347
346
|
| Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
|
|
348
347
|
| Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
|
|
349
348
|
| Security | [SECURITY.md](SECURITY.md) |
|
|
350
|
-
| Current published (4.8.
|
|
349
|
+
| Current published (4.8.20 on npm `latest`) | [CHANGELOG](CHANGELOG.md) |
|
|
351
350
|
| Prior published (4.8.11) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
|
|
352
351
|
| Prior published (4.8.10) | [docs/releases/4.8.10.md](docs/releases/4.8.10.md) · [CHANGELOG](CHANGELOG.md) |
|
|
353
352
|
| Prior published (4.8.9) | [docs/releases/4.8.9.md](docs/releases/4.8.9.md) · [CHANGELOG](CHANGELOG.md) |
|
|
@@ -1673,6 +1673,27 @@ async function main() {
|
|
|
1673
1673
|
inventory.candidates[0] != null
|
|
1674
1674
|
? inventoryToExtractionCard(inventory.candidates[0])
|
|
1675
1675
|
: null;
|
|
1676
|
+
let evidenceLines = [];
|
|
1677
|
+
let coverageEvidence = null;
|
|
1678
|
+
if (config.arkRules && Object.keys(config.arkRules).length > 0) {
|
|
1679
|
+
try {
|
|
1680
|
+
const { summarizeRulesUnderContract, formatArkRulesEvidenceLines } = await import(
|
|
1681
|
+
'./lib/rules-under-contract.mjs'
|
|
1682
|
+
);
|
|
1683
|
+
const section = summarizeRulesUnderContract(root, config, {
|
|
1684
|
+
files: files.map((file) => ({ path: normalize(path.relative(root, file)) })),
|
|
1685
|
+
});
|
|
1686
|
+
evidenceLines = formatArkRulesEvidenceLines(section);
|
|
1687
|
+
if (section.active === true) {
|
|
1688
|
+
coverageEvidence = {
|
|
1689
|
+
symbolEvidence: section.symbolEvidence ?? [],
|
|
1690
|
+
discarded: section.coverageStats?.discarded ?? null,
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
/* inventory still useful when coverage cannot be read */
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1676
1697
|
const payload = {
|
|
1677
1698
|
rulesInventory: inventory,
|
|
1678
1699
|
rulesMigration: {
|
|
@@ -1682,6 +1703,7 @@ async function main() {
|
|
|
1682
1703
|
notAScore: true,
|
|
1683
1704
|
},
|
|
1684
1705
|
nextPilot: nextPilot,
|
|
1706
|
+
...(coverageEvidence ? { coverageEvidence } : {}),
|
|
1685
1707
|
};
|
|
1686
1708
|
if (args.json) {
|
|
1687
1709
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -1695,6 +1717,7 @@ async function main() {
|
|
|
1695
1717
|
if (nextPilot) {
|
|
1696
1718
|
console.log(`Next extraction pilot: ${nextPilot.pilot} → ${nextPilot.pilotTarget}`);
|
|
1697
1719
|
}
|
|
1720
|
+
for (const line of evidenceLines) console.log(line);
|
|
1698
1721
|
}
|
|
1699
1722
|
process.exitCode = 0;
|
|
1700
1723
|
return;
|
package/bin/ark-layer-match.mjs
CHANGED
|
@@ -215,6 +215,14 @@ export function layerForRelativePath(relPath, layers) {
|
|
|
215
215
|
* Includes the parent folder so `features/auth` ≠ `modules/auth`.
|
|
216
216
|
* Identity is case-normalized for portable results across filesystems.
|
|
217
217
|
* `src/features/auth/api.ts` + folders `["features"]` → `"features/auth"`.
|
|
218
|
+
*
|
|
219
|
+
* A bare name stays that unanchored one-segment match. The child segment is
|
|
220
|
+
* never the filename, so a flat file under the parent is not its own slice.
|
|
221
|
+
* A starred prefix (`lib/features` plus one star per extra directory) reuses
|
|
222
|
+
* the shared-root anchor: it must sit at offset 0, or at offset 1 after `src/`
|
|
223
|
+
* or `app/`. The slice id is the concrete directories the stars bind. A star
|
|
224
|
+
* never binds the filename; a file directly under the last bound directory
|
|
225
|
+
* keeps that directory as its slice.
|
|
218
226
|
*/
|
|
219
227
|
export function sliceIdForPath(relPath, sliceFolders) {
|
|
220
228
|
if (!sliceFolders?.length)
|
|
@@ -222,11 +230,87 @@ export function sliceIdForPath(relPath, sliceFolders) {
|
|
|
222
230
|
const parts = String(relPath)
|
|
223
231
|
.split(/[/\\]/)
|
|
224
232
|
.filter(Boolean);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
233
|
+
if (parts.length === 0)
|
|
234
|
+
return undefined;
|
|
235
|
+
const bare = new Set();
|
|
236
|
+
for (const raw of sliceFolders) {
|
|
237
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
238
|
+
continue;
|
|
239
|
+
if (isAnchoredSliceEntry(raw)) {
|
|
240
|
+
const anchored = anchoredSliceId(parts, raw);
|
|
241
|
+
if (anchored)
|
|
242
|
+
return anchored;
|
|
243
|
+
continue;
|
|
229
244
|
}
|
|
245
|
+
if (!raw.includes('/') && !raw.includes('\\') && !raw.includes('*')) {
|
|
246
|
+
bare.add(raw.toLowerCase());
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return bareSliceId(parts, bare);
|
|
250
|
+
}
|
|
251
|
+
/** A starred prefix with a real leading directory, not a bare name and not `*`. */
|
|
252
|
+
function isAnchoredSliceEntry(entry) {
|
|
253
|
+
const segments = entry.split(/[/\\]/).filter((part) => part.length > 0);
|
|
254
|
+
if (segments.length < 2)
|
|
255
|
+
return false;
|
|
256
|
+
if (segments[0] === '*' || segments[0] === '**')
|
|
257
|
+
return false;
|
|
258
|
+
return segments.some((part) => part === '*');
|
|
259
|
+
}
|
|
260
|
+
function anchoredSliceId(parts, raw) {
|
|
261
|
+
const pattern = raw
|
|
262
|
+
.split(/[/\\]/)
|
|
263
|
+
.filter((part) => part.length > 0)
|
|
264
|
+
.map((part) => part.toLowerCase());
|
|
265
|
+
const offsets = anchorOffsets(parts, pattern[0] ?? '');
|
|
266
|
+
for (const offset of offsets) {
|
|
267
|
+
const id = bindAnchoredSlice(parts, pattern, offset);
|
|
268
|
+
if (id)
|
|
269
|
+
return id;
|
|
270
|
+
}
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Walk the prefix. Literals must match. Each star binds one directory and
|
|
275
|
+
* stops before the filename; leftover stars then end the id at the last
|
|
276
|
+
* directory that did bind.
|
|
277
|
+
*/
|
|
278
|
+
function bindAnchoredSlice(parts, pattern, offset) {
|
|
279
|
+
const bound = [];
|
|
280
|
+
let index = offset;
|
|
281
|
+
for (let pi = 0; pi < pattern.length; pi += 1) {
|
|
282
|
+
const segment = pattern[pi];
|
|
283
|
+
if (segment === '*') {
|
|
284
|
+
if (index >= parts.length - 1) {
|
|
285
|
+
for (let rest = pi; rest < pattern.length; rest += 1) {
|
|
286
|
+
if (pattern[rest] !== '*')
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
bound.push(parts[index].toLowerCase());
|
|
292
|
+
index += 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (segment === '**' || index >= parts.length)
|
|
296
|
+
return undefined;
|
|
297
|
+
if (parts[index].toLowerCase() !== segment)
|
|
298
|
+
return undefined;
|
|
299
|
+
bound.push(parts[index].toLowerCase());
|
|
300
|
+
index += 1;
|
|
301
|
+
}
|
|
302
|
+
return bound.length > 0 ? bound.join('/') : undefined;
|
|
303
|
+
}
|
|
304
|
+
/** Bare names: leftmost folder, plus the next directory. Never the filename. */
|
|
305
|
+
function bareSliceId(parts, names) {
|
|
306
|
+
if (names.size === 0)
|
|
307
|
+
return undefined;
|
|
308
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
309
|
+
if (!names.has(parts[i].toLowerCase()))
|
|
310
|
+
continue;
|
|
311
|
+
if (i + 1 >= parts.length - 1)
|
|
312
|
+
continue;
|
|
313
|
+
return `${parts[i].toLowerCase()}/${parts[i + 1].toLowerCase()}`;
|
|
230
314
|
}
|
|
231
315
|
return undefined;
|
|
232
316
|
}
|
|
@@ -279,6 +363,16 @@ function trimTrailingSlashes(value) {
|
|
|
279
363
|
}
|
|
280
364
|
/** Source folders a declared shared root may sit under without being named. */
|
|
281
365
|
const SHARED_ROOT_SOURCE_PREFIXES = ['src', 'app'];
|
|
366
|
+
/**
|
|
367
|
+
* Anchor at segment 0, or also at segment 1 when the path opens with `src/` or
|
|
368
|
+
* `app/` and the declaration does not itself start with that folder.
|
|
369
|
+
* Shared roots and starred slice prefixes share this offset.
|
|
370
|
+
*/
|
|
371
|
+
function anchorOffsets(parts, rootHead) {
|
|
372
|
+
const head = (parts[0] ?? '').toLowerCase();
|
|
373
|
+
const root = rootHead.toLowerCase();
|
|
374
|
+
return SHARED_ROOT_SOURCE_PREFIXES.includes(head) && root !== head ? [0, 1] : [0];
|
|
375
|
+
}
|
|
282
376
|
/** A root that would disable the wall wholesale is not a root. */
|
|
283
377
|
function isBlanketRoot(raw) {
|
|
284
378
|
const trimmed = trimTrailingSlashes(raw.replace(/^[./]+/, ''));
|
|
@@ -287,7 +381,7 @@ function isBlanketRoot(raw) {
|
|
|
287
381
|
/**
|
|
288
382
|
* Is `relPath` under one of the roots the rule declares shared on purpose?
|
|
289
383
|
*
|
|
290
|
-
* **Anchored
|
|
384
|
+
* **Anchored**: the root must start the repo-relative
|
|
291
385
|
* path, optionally after a single conventional source folder, so `ui` covers
|
|
292
386
|
* `ui/button.tsx` and `src/ui/button.tsx` but NOT `modules/a/ui/x.tsx` — an
|
|
293
387
|
* unanchored root would exempt a whole tree the author never declared. Deeper
|
|
@@ -317,9 +411,7 @@ export function pathUnderSharedRoot(relPath, sharedRoots) {
|
|
|
317
411
|
const root = normalizeSegments(raw);
|
|
318
412
|
if (root.length === 0)
|
|
319
413
|
continue;
|
|
320
|
-
|
|
321
|
-
// folder the root does not itself name.
|
|
322
|
-
const offsets = SHARED_ROOT_SOURCE_PREFIXES.includes(parts[0]) && root[0] !== parts[0] ? [0, 1] : [0];
|
|
414
|
+
const offsets = anchorOffsets(parts, root[0] ?? '');
|
|
323
415
|
for (const offset of offsets) {
|
|
324
416
|
if (offset + root.length > parts.length)
|
|
325
417
|
continue;
|
|
@@ -364,7 +456,8 @@ export function crossSliceEdgeAllowed(allowedCrossSlice, fromSlice, toSlice) {
|
|
|
364
456
|
* cross-slice edge, is the repo telling us its design, so it is no longer
|
|
365
457
|
* "unclassifiable". Order: no fromPath → no slice folders → intent (no toPath):
|
|
366
458
|
* shared-root allow / slice fail-closed → a side that is neither in a slice nor
|
|
367
|
-
* declared shared →
|
|
459
|
+
* declared shared → shared root importing a slice (only when the rule denies
|
|
460
|
+
* that hop) → same slice → declared cross edge → deny.
|
|
368
461
|
*
|
|
369
462
|
* Intent/event names are not files. Callers pass fromPath only and must not
|
|
370
463
|
* invent a toPath (that would mis-slice). A declared shared root is classified
|
|
@@ -386,6 +479,14 @@ export function peerIsolationDecision(input) {
|
|
|
386
479
|
const toClassified = Boolean(input.toSlice) || input.toShared === true;
|
|
387
480
|
if (!fromClassified || !toClassified)
|
|
388
481
|
return { denied: true, reason: 'unclassifiable-path' };
|
|
482
|
+
// Shared roots are a sink. The hop is allowed unless the rule opts in.
|
|
483
|
+
// allowedCrossSlice does not excuse it: this is not a slice-to-slice edge.
|
|
484
|
+
if (input.sharedImportsSlice === 'deny' &&
|
|
485
|
+
input.fromShared === true &&
|
|
486
|
+
Boolean(input.toSlice) &&
|
|
487
|
+
!input.fromSlice) {
|
|
488
|
+
return { denied: true, reason: 'shared-imports-slice' };
|
|
489
|
+
}
|
|
389
490
|
// At least one side is declared shared (and carries no slice id): the repo
|
|
390
491
|
// said this code belongs to no slice, so there is no cross-slice edge here.
|
|
391
492
|
if (!input.fromSlice || !input.toSlice)
|
|
@@ -416,6 +517,8 @@ export function peerIsolationDenyExplanation(reason, context) {
|
|
|
416
517
|
switch (reason) {
|
|
417
518
|
case 'cross-slice':
|
|
418
519
|
return `cross-slice edge ${context.fromSlice ?? '?'} → ${context.toSlice ?? '?'}. Extract the shared code, use events/ports across slices, or declare the edge in the rule's allowedCrossSlice.`;
|
|
520
|
+
case 'shared-imports-slice':
|
|
521
|
+
return `shared root imports slice ${context.toSlice ?? '?'} (${context.fromPath ?? '?'} → ${context.toPath ?? '?'}). The wall is direct-only.`;
|
|
419
522
|
case 'unclassifiable-path': {
|
|
420
523
|
const unplaced = [
|
|
421
524
|
context.fromSlice ? undefined : context.fromPath,
|
|
@@ -474,6 +577,7 @@ export function findDeniedEdgeDecision(rules, from, to, options) {
|
|
|
474
577
|
fromShared: !fromSlice && pathUnderSharedRoot(fromPath, rule.sharedRoots),
|
|
475
578
|
toShared: !toSlice && pathUnderSharedRoot(toPath, rule.sharedRoots),
|
|
476
579
|
crossSliceAllowed: crossSliceEdgeAllowed(rule.allowedCrossSlice, fromSlice, toSlice),
|
|
580
|
+
sharedImportsSlice: rule.sharedImportsSlice,
|
|
477
581
|
});
|
|
478
582
|
if (decision.denied) {
|
|
479
583
|
return { rule, peerIsolationReason: decision.reason, fromSlice, toSlice };
|
|
@@ -487,6 +591,34 @@ export function findDeniedEdgeDecision(rules, from, to, options) {
|
|
|
487
591
|
}
|
|
488
592
|
return undefined;
|
|
489
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Shared-root → slice edges the check still allows. Listed so a green run
|
|
596
|
+
* says the wall is direct-only. When the rule sets `sharedImportsSlice: "deny"`
|
|
597
|
+
* the same hop is a violation instead, so it is not listed twice.
|
|
598
|
+
*/
|
|
599
|
+
export function findSharedImportsSliceBridge(rules, from, to, options) {
|
|
600
|
+
const fromPath = options?.fromPath;
|
|
601
|
+
const toPath = options?.toPath;
|
|
602
|
+
if (!fromPath || !toPath)
|
|
603
|
+
return undefined;
|
|
604
|
+
for (const rule of rules ?? []) {
|
|
605
|
+
if (rule.from !== from || rule.to !== to)
|
|
606
|
+
continue;
|
|
607
|
+
if (rule.allowed !== false || !rule.peerIsolation)
|
|
608
|
+
continue;
|
|
609
|
+
if (rule.sharedImportsSlice === 'deny')
|
|
610
|
+
continue;
|
|
611
|
+
const folders = resolveSliceFolders(rule, from, options?.layers);
|
|
612
|
+
const fromSlice = sliceIdForPath(fromPath, folders);
|
|
613
|
+
const toSlice = sliceIdForPath(toPath, folders);
|
|
614
|
+
if (fromSlice || !toSlice)
|
|
615
|
+
continue;
|
|
616
|
+
if (!pathUnderSharedRoot(fromPath, rule.sharedRoots))
|
|
617
|
+
continue;
|
|
618
|
+
return { fromPath, toPath, toSlice };
|
|
619
|
+
}
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
490
622
|
export function isEdgeDenied(rules, from, to, options) {
|
|
491
623
|
return findDeniedEdgeRule(rules, from, to, options) !== undefined;
|
|
492
624
|
}
|
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -2037,8 +2037,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2037
2037
|
'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
|
|
2038
2038
|
'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
|
|
2039
2039
|
'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
|
|
2040
|
-
'Call BEFORE writing a new file. ' +
|
|
2041
|
-
'Prefer ark_prepare_write when you already have the source snippet
|
|
2040
|
+
'Call BEFORE writing a new file to learn the layer. ' +
|
|
2041
|
+
'Prefer ark_prepare_write only as fallback when the host has no hard pre-hook (or that hook is missing / fail-open) and you already have the source snippet.',
|
|
2042
2042
|
inputSchema: {
|
|
2043
2043
|
type: 'object',
|
|
2044
2044
|
properties: {
|
|
@@ -2061,7 +2061,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2061
2061
|
'constrain (layer, mayImport, mustNotImport, forbiddenGlobals) + validate source + optional ' +
|
|
2062
2062
|
'mechanical-safe autoPatch + judgmentBrief when judgment is needed + contentHash for host commit. ' +
|
|
2063
2063
|
'Also returns the versioned new/worsened designDelta for the proposed full file. ' +
|
|
2064
|
-
'Composes ark_place + write-gate
|
|
2064
|
+
'Composes ark_place + write-gate. Fallback when the host has no trusted hard pre-hook (or that hook is fail-open). When a hard hook is installed, the host already gates the write; this tool stays available but is not the primary gate. ' +
|
|
2065
2065
|
'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
|
|
2066
2066
|
'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
|
|
2067
2067
|
'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
|