arkgate 4.8.23 → 4.8.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +24 -77
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +596 -112
  6. package/bin/ark-mcp-runtime.mjs +412 -138
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +39 -0
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/agent-projection-command.mjs +5 -1
  13. package/bin/lib/analysis-engine.mjs +15 -11
  14. package/bin/lib/architecture-scan.mjs +50 -6
  15. package/bin/lib/ark-order-doctor.mjs +1 -1
  16. package/bin/lib/ark-order-facts.mjs +410 -15
  17. package/bin/lib/ark-order-invariants.mjs +51 -9
  18. package/bin/lib/ark-order-sensors.mjs +28 -101
  19. package/bin/lib/ark-order-types.mjs +2 -0
  20. package/bin/lib/ark-run-doctor.mjs +14 -0
  21. package/bin/lib/ark-run-facts.mjs +434 -11
  22. package/bin/lib/ark-run-sensors.mjs +5 -3
  23. package/bin/lib/arkgate-pins.mjs +91 -0
  24. package/bin/lib/arkrules-sensors.mjs +50 -249
  25. package/bin/lib/check-args.mjs +23 -1
  26. package/bin/lib/ci-and-commands.mjs +16 -6
  27. package/bin/lib/class-source-scan.mjs +479 -0
  28. package/bin/lib/codex-home.mjs +4 -2
  29. package/bin/lib/config-contract-slices.mjs +701 -0
  30. package/bin/lib/config-contract.mjs +5 -493
  31. package/bin/lib/config-warnings.mjs +32 -0
  32. package/bin/lib/dashboard-cli.mjs +62 -0
  33. package/bin/lib/design-delta.mjs +1 -1
  34. package/bin/lib/diagnostic-catalog.mjs +16 -9
  35. package/bin/lib/doctor-advisories.mjs +25 -1
  36. package/bin/lib/doctor-next-actions.mjs +17 -2
  37. package/bin/lib/doctor-plan.mjs +6 -8
  38. package/bin/lib/effective-contract-load.mjs +121 -59
  39. package/bin/lib/enforcement-profiles.mjs +40 -4
  40. package/bin/lib/first-run-help.mjs +3 -2
  41. package/bin/lib/flat-parent-importers.mjs +137 -0
  42. package/bin/lib/flat-parent-pilot.mjs +40 -100
  43. package/bin/lib/gate-files.mjs +29 -2
  44. package/bin/lib/git-change-scope.mjs +109 -0
  45. package/bin/lib/github-enforcement.mjs +1 -1
  46. package/bin/lib/hook-templates.mjs +18 -16
  47. package/bin/lib/host-support-matrix.mjs +3 -1
  48. package/bin/lib/import-resolve.mjs +13 -1
  49. package/bin/lib/install-migrate.mjs +50 -73
  50. package/bin/lib/invariant-coverage.mjs +59 -11
  51. package/bin/lib/mcp-adoption.mjs +9 -1
  52. package/bin/lib/mcp-hook-payload.mjs +401 -61
  53. package/bin/lib/package-manager.mjs +59 -1
  54. package/bin/lib/passthrough-commands.mjs +83 -0
  55. package/bin/lib/persistence-write-hint.mjs +81 -0
  56. package/bin/lib/pin-dependent-gates.mjs +117 -0
  57. package/bin/lib/policy-delta-io.mjs +168 -10
  58. package/bin/lib/prepare-change.mjs +120 -6
  59. package/bin/lib/prepare-write.mjs +1 -0
  60. package/bin/lib/presets.mjs +3 -5
  61. package/bin/lib/remediation.mjs +23 -4
  62. package/bin/lib/resolved-candidate-facts.mjs +315 -55
  63. package/bin/lib/rules-inventory.mjs +22 -3
  64. package/bin/lib/rules-under-contract.mjs +179 -5
  65. package/bin/lib/scan-files.mjs +32 -0
  66. package/bin/lib/snippet-analysis.mjs +140 -6
  67. package/bin/lib/source-policy.mjs +34 -0
  68. package/bin/lib/stable-hash.mjs +90 -0
  69. package/bin/lib/start-install-recovery.mjs +71 -1
  70. package/bin/lib/start-preview.mjs +30 -0
  71. package/bin/lib/status-command.mjs +33 -4
  72. package/bin/lib/status-manifest.mjs +32 -4
  73. package/bin/lib/team-parliament-io.mjs +77 -75
  74. package/bin/lib/team-parliament.mjs +6 -1
  75. package/bin/lib/write-path-capabilities.mjs +4 -2
  76. package/dist/chunk-A5EIY3HI.cjs +5 -0
  77. package/dist/chunk-MBXCFLO4.js +16 -0
  78. package/dist/chunk-RNQHRDCQ.cjs +16 -0
  79. package/dist/chunk-SVSQVGFJ.js +5 -0
  80. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-BJbaeoj4.d.cts} +18 -3
  81. package/dist/configTypes-BJbaeoj4.d.ts +246 -0
  82. package/dist/eslint/index.cjs +17 -4
  83. package/dist/eslint/index.d.cts +359 -0
  84. package/dist/eslint/index.d.ts +130 -156
  85. package/dist/eslint/index.js +10 -4
  86. package/dist/index.cjs +35 -34
  87. package/dist/index.d.cts +5475 -0
  88. package/dist/index.d.ts +3312 -299
  89. package/dist/index.js +35 -34
  90. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  91. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  92. package/dist/nestjs/index.cjs +1 -18
  93. package/dist/nestjs/index.d.cts +22 -0
  94. package/dist/nestjs/index.d.ts +1 -3
  95. package/dist/nestjs/index.js +1 -18
  96. package/dist/order/index.cjs +1 -1
  97. package/dist/order/index.d.cts +155 -0
  98. package/dist/order/index.d.ts +15 -3
  99. package/dist/order/index.js +1 -1
  100. package/dist/runtime/index.cjs +13 -30
  101. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +804 -290
  102. package/dist/runtime/index.d.ts +2678 -108
  103. package/dist/runtime/index.js +13 -30
  104. package/dist/{types-Bdj32QwA.d.ts → types-DRinG-vT.d.cts} +576 -140
  105. package/dist/types-DRinG-vT.d.ts +1792 -0
  106. package/docs/README.md +2 -1
  107. package/docs/agent-guide.md +84 -15
  108. package/docs/ai-gates.md +160 -26
  109. package/docs/arkorder.md +32 -15
  110. package/docs/brownfield-adoption.md +5 -1
  111. package/docs/configuration.md +196 -26
  112. package/docs/develop.md +2 -1
  113. package/docs/diagnostics.md +96 -19
  114. package/docs/package-surface.md +31 -8
  115. package/docs/product-voice.md +1 -1
  116. package/package.json +59 -18
  117. package/schemas/ark.analysis-result.schema.json +15 -0
  118. package/schemas/ark.config.schema.json +47 -10
  119. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  120. package/schemas/ark.status-manifest.schema.json +22 -0
  121. package/server.json +3 -9
  122. package/templates/agent-skills/README.md +1 -1
  123. package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
  124. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  125. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  126. package/templates/arkrules/PersistenceAdapters.json +1 -1
  127. package/templates/arkrules/PresentationAdapters.json +1 -1
  128. package/templates/skills/ark-adopt.md +4 -1
  129. package/templates/skills/ark-order.md +4 -2
  130. package/templates/skills/ark-runtime.md +6 -1
  131. package/dist/types-Ik_OakcI.d.ts +0 -183
package/CHANGELOG.md CHANGED
@@ -96,6 +96,74 @@ 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.24 — 2026-09-29
100
+
101
+ **Patch** over **4.8.23**. Follow-ups to the hierarchical slices shipped in 4.8.23
102
+ (issues #335–#338), every defect a full audit of the gate and its add-ons
103
+ confirmed, and much lower memory per validation (#339). **Write. Check. Ship.**
104
+ **No required config migration:** a config that loads in 4.8.23 still loads;
105
+ ids that changed meaning warn (`CONFIG_CHILD_SLICES_INERT`,
106
+ `CONFIG_SLICE_LEGACY_STARS_ID`) and keep their 4.8.23 verdict for one release.
107
+ No `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
108
+ `ark.config.json` still does **not** turn `arkOrder` on.
109
+
110
+ **Minimum version 4.8.24 for the new config fields** (`sharedImportsSlice`
111
+ object form with `stopAt`, `childSlices.message`, `childSlices.siblings.ratchet`).
112
+ arkgate 4.8.23 and older reject them at config load. Pin 4.8.24 in the write
113
+ hook and CI first.
114
+
115
+ **Status: released** (tag `v4.8.24`). This file does not assert registry
116
+ state; run `npm view arkgate@4.8.24 version` to confirm (#314).
117
+
118
+ ### Added
119
+ - `sharedImportsSlice` accepts `{ "mode": "deny-cross-parent", "stopAt": [...] }`. The whole-graph `CROSS_PARENT_VIA_SHARED` walk never starts at or passes through a stop file, so a declared composition root (`kernel/bootstrap.ts`, `kernel/registrations/**`) is no longer reported as a crossing. `stopAt` matches like a shared root or an alias glob; a stop that covers the whole tree or a whole layer root is rejected. Findings carry `via`, and doctor `sharedWalkHubs` names a likely undeclared composition root. Adding stops is a weakening policy delta. The string forms are unchanged. arkgate 4.8.23 and older reject the object (issue [#335](https://github.com/pedroknigge/arkgate/issues/335)).
120
+ - Optional `childSlices.message` sets the text for inner-wall findings (`CROSS_SIBLING_SLICE`, universe common code importing a child). Without it ArkGate uses its own text; the rule `message` stays the universe-wall text and is never reused on inner-wall findings. Same text in ark-check, CI, the write hook, MCP, and ESLint (issue [#337](https://github.com/pedroknigge/arkgate/issues/337)).
121
+ - Optional `childSlices.siblings.ratchet`: `true` always fails a new advisory crossing, `false` only measures (issue [#336](https://github.com/pedroknigge/arkgate/issues/336)).
122
+ - `arkgate mcp` / `ark mcp` start the stdio MCP server (same as `arkgate-mcp`).
123
+ - ESLint: warn-level `ark/architecture-advisory` rule (findings ark-check reports as warnings) and `ark/arkrules-structure` (ArkRules structure sensors on the same Effective Contract).
124
+ - New advisory `ARKRULE_FILE_UNREFERENCED` for an `arkrules/*.json` the config does not reference, and `doctor.rulesMigration`.
125
+ - `ark-dashboard --help`, `--version`, `--once`, `ARK_DASHBOARD_URL`.
126
+ - `npm run bench:memory` regression guard (budgets in `eval/performance/memory-budgets.v1.json`).
127
+
128
+ ### Changed
129
+ - **Memory and speed.** ark-check, `--doctor`, the write hook, ApplyPatch preflight, the MCP server and the ESLint plugin use much less memory; verdicts are byte-identical. On a 20k-file repo: ark-check peak 422 → 303 MB (3.7 → 2.3 s), ApplyPatch hook 649 → 414 MB (9.5 → 3.6 s), Write hook 163 → 146 MB (0.68 → 0.24 s), MCP server after 50 calls 725 → 442 MB. Sources are read one at a time and released, the facts hash streams, module resolution is memoized per run, the Write/Edit hook no longer walks the whole `include` tree for tsconfig aliases, and preflight reuses base parses.
130
+ - Advisory sibling crossings are advisory on day 1. The anti-growth ratchet is judged per rule and turns on only when the baseline in use already records an advisory sibling crossing of that rule (issue [#336](https://github.com/pedroknigge/arkgate/issues/336)).
131
+ - `CONFIG_CHILD_SLICES_VERSION` (and the other new-key version warnings) fire only with evidence that the repo pins an older arkgate (exact `package.json` dependency, installed copy, lockfile, hook script, or CI workflow). The finding points at that file and line and names the version to bump to (issue [#338](https://github.com/pedroknigge/arkgate/issues/338)).
132
+ - `childSlices` on a rule without `peerIsolation: true` and `allowed: false` still loads (as in 4.8.23) but is inert; ark-check now warns `CONFIG_CHILD_SLICES_INERT` and says how to fix it (add both flags to the rule, or remove `childSlices`). No config that loads in 4.8.23 fails to load.
133
+ - `sliceIdentity: "stars"` keeps every star binding (`modules/*/api/*` binds `orders/api/v1`, not `api/v1`). **Migration:** a `sliceAliases` `to` or an `allowedCrossSlice` / `childSlices.allowedCrossSlice` entry written against the 4.8.23 id (`api/v1/x`, `api/v1`) keeps loading and keeps its 4.8.23 verdict for this release: the alias joins the universe whose old id it names, and the allowance still clears the edge it cleared. ark-check warns `CONFIG_SLICE_LEGACY_STARS_ID` and names the new id to write (`orders/api/v1/x`). A legacy alias target is rejected at config load only when it maps to more than one universe shape of the rule. The legacy form is removed in the next minor release.
134
+ - `childSlices.commonFolders` counts only a folder directly under the universe; the same name inside a child belongs to that child.
135
+ - The write hook fails closed: if it cannot run inside an Ark project (invalid config, missing ArkRules file, missing `dist/`), governed writes are denied with exit 2 (`WRITE_GATE_UNAVAILABLE`) instead of exit 1, which hosts let through. The hook finds its project root like ark-check: a set `--root-env` variable, else the root that holds `ark.config.json`, else it walks up from the payload (written file, `cwd`, `workspace_roots`) to the nearest `ark.config.json`. A write outside any Ark project (no `ark.config.json` found) is allowed, as in 4.8.23.
136
+ - `ark start --apply --remove-host <host>` removes only the Ark hook entries from a merged `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json` or `.agents/hooks.json` (same identity as the merge) and keeps every user key and hook; the file is deleted only when nothing else remains. "customized" is reported only when the Ark entry itself was edited.
137
+ - An enforced invariant with `coverage: { test: false }` and no `coverage.symbol` is uncovered; `coverage.symbol: "Class.member"` needs the member declared in that class.
138
+ - ArkOrder `apply()` only accepts a proposal bound to the current Release (`ARKORDER_STALE_PROPOSAL`); `restore()` never changes ξ or rolls the version back; `ReleaseStore.load()` is validated; the plane advances only after `save()`; `informationBudget` is enforced when a Release is created.
139
+ - ArkRun: the default event buffer is capped and settles after local delivery; broker handoff failures are recorded (`event.handoffFailed`); config kernels with canonical layer names enforce hard observed-layer-flow.
140
+ - `ark-check --update-baseline` always freezes the whole tree and refuses `--changed` / `--local`.
141
+
142
+ ### Fixed
143
+ - `--changed`, `--local`, `--against` and `--persona` no longer give a false green: paths resolve relative to `--root` (monorepo packages), non-ASCII names are kept, large or failed git listings exit 2, and a missing base ref exits 2 (`changed-needs-base`) instead of "nothing changed".
144
+ - `--changed` catches a new `CROSS_PARENT_VIA_SHARED` created by editing only a shared-root file, and no longer warns `CONFIG_LAYER_PATTERN_NO_MATCHES` for untouched layers.
145
+ - `ark-check --report`, `latest.json`, `--doctor` and `ark status` agree on frozen versus active findings.
146
+ - Write path parity with CI: the hook, `validate_code`, `ark_prepare_write`, `ark_prepare_change` and ApplyPatch preflight enforce ArkRules structure sensors and ArkOrder editor sensors, load ArkRules like ark-check, handle realpath aliases, and accept the full Codex `apply_patch` format.
147
+ - MCP: `validate_code` / `ark_check` output schemas pass MCP SDK validation (tools no longer disappear); a stale contract returns `CONTRACT_STALE`; `ark_status` honors `--config`; `arkgate-mcp --help` / `--version` exit.
148
+ - MCP Registry: `server.json` runs `npx arkgate@<version> mcp --root .`; the old descriptor never started the server.
149
+ - CommonJS TypeScript consumers: `.d.cts` declarations ship and are mapped for every entry.
150
+ - ESLint: warnings in ark-check are warnings in ESLint; messages carry the rule message and slice reason; ArkOrder rules show real text at 1-based columns; tsconfig JSONC / nearest tsconfig / `extends` are read correctly; an invalid config reports one diagnostic instead of crashing; `require('arkgate/eslint')` returns the plugin (it also carries a non-enumerable `__esModule`, so Babel `_interopRequireWildcard` and TypeScript `__importStar` still see every named export); `--cache` invalidates on contract change; forbidden-globals catches destructuring and bracket access.
151
+ - Slices: adapter diagnostics carry `reasonId` and the universe pair; `CROSS_PARENT_VIA_SHARED` honors the universe `allowedCrossSlice`; fail-closed reasons are named in the hook and MCP; folder-form `sliceAliases` cover their subtree; the flat-parent move card counts every resolved importer; `SHARED_IMPORTS_SLICE` is catalogued.
152
+ - Setup: `ark start --archetype/--preset` writes that shape; re-running `ark start --apply` works; `--remove-host` does not touch packages; existing `.claude/settings.json` / `.codex/hooks.json` are merged instead of skipped or wiped; `typecheck` is added only when TypeScript is present; the steward lock holds under `--strict-merge --contract-session`; `ark status --config` / `ark agents-md --config` work; starter ArkRules ids are unique.
153
+ - ArkRules: policy delta reads base ArkRules from the base ref (demoting an enforced rule is weakening); class-shape sensors see generic classes, ignore braces in strings and regexes, and skip a `static { … }` initialization block (it is not a public member); `ArkRulesValidationError` is exported from the root.
154
+ - ArkRun: undeclared emit/handle/depend count only kernel-traced receivers (`res.send`, `require.resolve`, rxjs `subscribe` are ignored); typed `send(Creator)` resolves names; `ArkModule.forRoot()` is a composition root; `arkgate/runtime` and `arkgate/nestjs` share one kernel chunk (`instanceof` works). A namespace import of a composition root (`import * as main from '../main'; main.ark.publisher(..)`) is traced as a kernel receiver, and an untraced receiver counts when its name carries one of the project's effective intent prefixes (configured `layers[].intentPrefixes` plus the canonical ones), not only `Domain.`/`Application.`….
155
+ - ArkOrder: `ARKORDER_XI_FIELD_WRITE` recognizes Prisma/ORM clients by construction, `GENERIC_UPDATE` / `INGEST_WRITES_XI` false positives removed, `xi-ttl` and `information-budget` sensors emit, NaN/Infinity rejected, documented types exported, and the shipped `bin/lib/ark-order-invariants.mjs` loads.
156
+ - Docs: the composite Action pin tracks the current release, and configuration.md names the real policy-delta entry points.
157
+ - Generated CI workflows, host hooks and MCP entries in a project that does not
158
+ have arkgate as a local dependency run `npx -y -p arkgate@<exact version> <bin>`
159
+ instead of a bare `npx ark-check` / `npx arkgate-mcp` (npm resolved those as a
160
+ nonexistent package, or ran a stale global). Without a `package.json` the workflow
161
+ skips the install step and the npm cache. `--migrate-commands` rewrites bare runners
162
+ into the pinned form, and `--doctor` leads with `PACKAGE_PIN_ABSENT` when a generated
163
+ file still uses the local runner without the pin.
164
+ - `ark-check --json` answers every fatal error with a JSON envelope (`POLICY_BASE_UNREADABLE` when a CI base ref such as `GITHUB_BASE_REF` cannot be read in the checkout, otherwise `CHECK_ERROR`), not only `CONFIG_INVALID`; exit code stays 2.
165
+ - Security: CodeQL `js/polynomial-redos` in the steward email check and two incomplete-sanitization alerts are closed.
166
+
99
167
  ## 4.8.23 — 2026-09-28
100
168
 
101
169
  **Patch** over **4.8.22**. Hierarchical slices: the issue #326 series
package/README.md CHANGED
@@ -25,11 +25,11 @@ Design the boundary before you implement. Put a small interface in front of the
25
25
 
26
26
  ## Try it
27
27
 
28
- One command. Preview only — no files change. Current release is **4.8.23**
28
+ One command. Preview only — no files change. Current release is **4.8.24**
29
29
  (check npm `latest` with `npm view arkgate version`):
30
30
 
31
31
  ```bash
32
- npx arkgate@4.8.23 start
32
+ npx arkgate@4.8.24 start
33
33
  ```
34
34
 
35
35
  ## What a red result looks like
@@ -320,9 +320,11 @@ queue monitors:
320
320
 
321
321
  Poll those facts from the dual bins **`ark-dashboard`** / **`arkgate-dashboard`**
322
322
  (`bin/ark-dashboard.mjs`). ANSI + polling only (no React/Ink/Blessed). Point
323
- `--url` / `-u` at the inspector snapshot (default
324
- `http://127.0.0.1:3000/snapshot`); the dashboard also fetches sibling `/outbox`
325
- and `/workflows`. `--interval` / `-i` is clamped to 200–60000 ms (default 2000).
323
+ `--url` / `-u` at the inspector (`handle.url` or `handle.snapshotUrl`); the dashboard
324
+ also fetches sibling `/outbox` and `/workflows`. `startInspector()` binds a random port
325
+ unless you pass `{ port }`, so the fallback `http://127.0.0.1:3000/snapshot` (or
326
+ `ARK_DASHBOARD_URL`) only matches `startInspector({ port: 3000 })`. `--interval` / `-i`
327
+ is clamped to 200–60000 ms (default 2000); `--once`, `--help`, `--version` are supported.
326
328
  Also available as `ark dashboard` / `arkgate dashboard` (passthrough to the same bin). Kernel stays
327
329
  JSON-only; presentation stays in `bin/`.
328
330
 
@@ -106,7 +106,7 @@ import {
106
106
  normalize,
107
107
  walk,
108
108
  } from './lib/scan-files.mjs';
109
- import { configWarning } from './lib/config-warnings.mjs';
109
+ import { configInvalidJsonEnvelope, configWarning } from './lib/config-warnings.mjs';
110
110
  import { runArchitectureScan } from './lib/architecture-scan.mjs';
111
111
  import {
112
112
  ANALYSIS_COMPLETENESS,
@@ -124,6 +124,7 @@ import {
124
124
  applyAgainstRatchet,
125
125
  bindTeamBaseRefs,
126
126
  contractSessionFrom,
127
+ pruneScopedPatternWarnings,
127
128
  runTeamPreflight,
128
129
  ungovernedDumpMessage,
129
130
  } from './lib/team-parliament-io.mjs';
@@ -135,7 +136,9 @@ import {
135
136
  resolveEffectiveProjectRoot,
136
137
  } from './lib/project-root.mjs';
137
138
  import { demoteArkRuleTeethUnderClassificationFloor } from './lib/rules-under-contract.mjs';
138
- import { localCheckEnvelope, parseArgs, resolveDesignDeltaBaseRef } from './lib/check-args.mjs';
139
+ import {
140
+ effectiveBaselineName, localCheckEnvelope, parseArgs, resolveDesignDeltaBaseRef, UPDATE_BASELINE_SCOPE_MESSAGE,
141
+ } from './lib/check-args.mjs';
139
142
  import { detectConfig, proposeForUncovered } from './lib/check-config-detect.mjs';
140
143
  import { runWatchMode } from './lib/check-watch.mjs';
141
144
 
@@ -1333,6 +1336,7 @@ async function main() {
1333
1336
  strictMerge: args.strictMerge || args.contractDiff,
1334
1337
  basePath: args.policyBase,
1335
1338
  baseRef: args.policyBaseRef,
1339
+ adoptMissingBase: args.policyBaseFromTeam === true,
1336
1340
  acknowledgementPath: args.policyAck,
1337
1341
  });
1338
1342
  const preflight = runTeamPreflight({
@@ -1447,6 +1451,7 @@ async function main() {
1447
1451
  args,
1448
1452
  });
1449
1453
 
1454
+ if (args.changed) pruneScopedPatternWarnings(warnings, root, loadGovernedFiles);
1450
1455
  // Align merge with extraMergeTeeth stamp: demote enforced ArkRules under classification floor.
1451
1456
  const preCov = computeCoverage(root, config, files, rules);
1452
1457
  const populatedLayerCount = Array.isArray(preCov.layers)
@@ -1515,6 +1520,8 @@ async function main() {
1515
1520
  }
1516
1521
 
1517
1522
  if (args.updateBaseline) {
1523
+ // A persona can turn on --changed after parseArgs; a subset scan must never rewrite the baseline.
1524
+ if (args.changed) { console.error(UPDATE_BASELINE_SCOPE_MESSAGE); process.exitCode = 2; return; }
1518
1525
  if (!contractSessionFrom(args)) {
1519
1526
  console.error('Growing the baseline requires --contract-session. Freeze in a law-only PR.');
1520
1527
  process.exitCode = 1;
@@ -1587,14 +1594,16 @@ async function main() {
1587
1594
  changed: args.changed,
1588
1595
  changedPaths,
1589
1596
  occurrenceKeys: baselineOccurrenceKeys(violations),
1597
+ rules,
1598
+ layers: config.layers,
1590
1599
  });
1591
1600
  activeViolations = ratcheted.activeViolations;
1592
1601
  suppressed = ratcheted.suppressed;
1593
- } else if (args.baseline) {
1594
- const baseline = readBaseline(root, args.baseline);
1602
+ } else if (effectiveBaselineName(args, root)) {
1603
+ const baseline = readBaseline(root, effectiveBaselineName(args, root));
1595
1604
  if (baseline.exists) {
1596
1605
  const occurrenceKeys = baselineOccurrenceKeys(violations);
1597
- const judged = applyAdvisorySiblingRatchet(violations, occurrenceKeys, baseline.keys);
1606
+ const judged = applyAdvisorySiblingRatchet(violations, occurrenceKeys, baseline.keys, { rules, layers: config.layers });
1598
1607
  suppressed = judged.filter((_, index) => baseline.keys.has(occurrenceKeys[index]));
1599
1608
  activeViolations = judged.filter((_, index) => !baseline.keys.has(occurrenceKeys[index]));
1600
1609
  const currentKeys = new Set(occurrenceKeys);
@@ -1639,81 +1648,16 @@ async function main() {
1639
1648
  }
1640
1649
 
1641
1650
  if (args.rulesInventory) {
1642
- const { buildRulesInventory, inventoryToExtractionCard } = await import('./lib/rules-inventory.mjs');
1643
- const fileContents = {};
1644
- const fileLayers = {};
1645
- for (const file of files.slice(0, 400)) {
1646
- const rel = normalize(path.relative(root, file));
1647
- try {
1648
- fileContents[rel] = fs.readFileSync(file, 'utf8');
1649
- const layer = layerForFile(root, file, config.layers);
1650
- if (layer) fileLayers[rel] = layer;
1651
- } catch {
1652
- /* skip unreadable */
1653
- }
1654
- }
1655
- const contracted = [];
1656
- if (config.arkRules) {
1657
- try {
1658
- const { loadEffectiveArkRulesFromDisk } = await import('./lib/effective-contract-load.mjs');
1659
- const loaded = loadEffectiveArkRulesFromDisk(root, config);
1660
- for (const rule of loaded.arkRules.structure ?? []) contracted.push(rule.id);
1661
- for (const inv of loaded.arkRules.invariants ?? []) contracted.push(inv.id);
1662
- } catch {
1663
- /* advisory inventory still useful */
1664
- }
1665
- }
1666
- const inventory = buildRulesInventory({
1667
- fileContents,
1668
- fileLayers,
1669
- layerContexts: (config.layers ?? []).map((layer) => ({
1670
- name: layer.name,
1671
- intentPrefixes: layer.intentPrefixes ?? [],
1672
- })),
1673
- contractedRuleIds: contracted,
1674
- });
1675
- const nextPilot =
1676
- inventory.candidates[0] != null
1677
- ? inventoryToExtractionCard(inventory.candidates[0])
1678
- : null;
1679
- let evidenceLines = [];
1680
- let coverageEvidence = null;
1681
- if (config.arkRules && Object.keys(config.arkRules).length > 0) {
1682
- try {
1683
- const { summarizeRulesUnderContract, formatArkRulesEvidenceLines } = await import(
1684
- './lib/rules-under-contract.mjs'
1685
- );
1686
- const section = summarizeRulesUnderContract(root, config, {
1687
- files: files.map((file) => ({ path: normalize(path.relative(root, file)) })),
1688
- });
1689
- evidenceLines = formatArkRulesEvidenceLines(section);
1690
- if (section.active === true) {
1691
- coverageEvidence = {
1692
- symbolEvidence: section.symbolEvidence ?? [],
1693
- discarded: section.coverageStats?.discarded ?? null,
1694
- };
1695
- }
1696
- } catch {
1697
- /* inventory still useful when coverage cannot be read */
1698
- }
1699
- }
1700
- const payload = {
1701
- rulesInventory: inventory,
1702
- rulesMigration: {
1703
- inventoried: inventory.inventoried,
1704
- underContract: inventory.underContract,
1705
- frozen: inventory.frozen,
1706
- notAScore: true,
1707
- },
1708
- nextPilot: nextPilot,
1709
- ...(coverageEvidence ? { coverageEvidence } : {}),
1710
- };
1651
+ // Same helper as MCP ark_rules_inventory and doctor.rulesMigration (AR15).
1652
+ const { buildRulesInventoryPayload, formatRulesMigrationCounts } = await import(
1653
+ './lib/rules-under-contract.mjs'
1654
+ );
1655
+ const { payload, evidenceLines } = buildRulesInventoryPayload(root, config, files);
1656
+ const { rulesInventory: inventory, nextPilot } = payload;
1711
1657
  if (args.json) {
1712
1658
  console.log(JSON.stringify(payload, null, 2));
1713
1659
  } else {
1714
- console.log(
1715
- `Rules inventory: ${inventory.inventoried} inventoried, ${inventory.underContract} under contract, ${inventory.frozen} frozen (not a score).`
1716
- );
1660
+ console.log(`Rules inventory: ${formatRulesMigrationCounts(payload.rulesMigration)}.`);
1717
1661
  for (const c of inventory.candidates.slice(0, 12)) {
1718
1662
  console.log(` - [${c.confidence}] ${c.kind} @ ${c.file}:${c.line} — ${c.message}`);
1719
1663
  }
@@ -1865,6 +1809,7 @@ async function main() {
1865
1809
  ? renderBeginnerHtmlReport(reportPayload)
1866
1810
  : renderHtmlReport(reportPayload);
1867
1811
  const reportPath = path.isAbsolute(args.report) ? args.report : path.join(root, args.report);
1812
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
1868
1813
  fs.writeFileSync(reportPath, html);
1869
1814
 
1870
1815
  const archive = archiveReportSnapshots(root, {
@@ -2087,6 +2032,8 @@ async function main() {
2087
2032
  }
2088
2033
 
2089
2034
  main().catch((error) => {
2035
+ const envelope = configInvalidJsonEnvelope(error, process.argv);
2036
+ if (envelope) console.log(envelope);
2090
2037
  console.error(
2091
2038
  process.env.ARK_DEBUG_STACK === '1' && error instanceof Error
2092
2039
  ? error.stack
@@ -1,43 +1,55 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
2
3
  import { parseArgs } from 'node:util';
4
+ import {
5
+ DASHBOARD_DEFAULT_INTERVAL_MS,
6
+ DASHBOARD_DEFAULT_TIMEOUT_MS,
7
+ DASHBOARD_DEFAULT_URL,
8
+ DASHBOARD_EPHEMERAL_PORT_HINT,
9
+ DASHBOARD_HELP,
10
+ clampDashboardMs,
11
+ dashboardSiblingUrl,
12
+ } from './lib/dashboard-cli.mjs';
3
13
 
4
- const DEFAULT_INTERVAL_MS = '2000';
5
- const DEFAULT_SNAPSHOT_URL = 'http://127.0.0.1:3000/snapshot';
6
- const DEFAULT_FETCH_TIMEOUT_MS = 5_000;
7
14
  const MAX_FIELD_LEN = 120;
8
15
 
9
- const args = parseArgs({
10
- options: {
11
- interval: {
12
- type: 'string',
13
- short: 'i',
14
- default: DEFAULT_INTERVAL_MS
15
- },
16
- url: {
17
- type: 'string',
18
- short: 'u',
19
- default: DEFAULT_SNAPSHOT_URL
20
- },
21
- timeout: {
22
- type: 'string',
23
- short: 't',
24
- default: String(DEFAULT_FETCH_TIMEOUT_MS)
25
- }
26
- },
27
- allowPositionals: true
28
- });
16
+ function parseDashboardArgs() {
17
+ try {
18
+ return parseArgs({
19
+ options: {
20
+ interval: { type: 'string', short: 'i' },
21
+ url: { type: 'string', short: 'u' },
22
+ timeout: { type: 'string', short: 't' },
23
+ once: { type: 'boolean' },
24
+ help: { type: 'boolean', short: 'h' },
25
+ version: { type: 'boolean', short: 'v' },
26
+ },
27
+ allowPositionals: true,
28
+ });
29
+ } catch (err) {
30
+ const message = err instanceof Error ? err.message.split('. ')[0] : String(err);
31
+ process.stderr.write(`ark-dashboard: ${message}. Run ark-dashboard --help.\n`);
32
+ process.exit(2);
33
+ }
34
+ }
35
+
36
+ const args = parseDashboardArgs();
37
+ if (args.values.help || args.positionals[0] === 'help') {
38
+ console.log(DASHBOARD_HELP);
39
+ process.exit(0);
40
+ }
41
+ if (args.values.version) {
42
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
43
+ console.log(pkg.version);
44
+ process.exit(0);
45
+ }
29
46
 
30
- const parsedInterval = parseInt(args.values.interval, 10);
31
- const interval =
32
- Number.isFinite(parsedInterval) && parsedInterval >= 200 && parsedInterval <= 60_000
33
- ? parsedInterval
34
- : Number(DEFAULT_INTERVAL_MS);
35
- const targetUrl = args.values.url || DEFAULT_SNAPSHOT_URL;
36
- const parsedTimeout = parseInt(args.values.timeout, 10);
37
- const fetchTimeoutMs =
38
- Number.isFinite(parsedTimeout) && parsedTimeout >= 200 && parsedTimeout <= 60_000
39
- ? parsedTimeout
40
- : DEFAULT_FETCH_TIMEOUT_MS;
47
+ const envUrl = process.env.ARK_DASHBOARD_URL?.trim();
48
+ const urlExplicit = Boolean(args.values.url || envUrl);
49
+ const targetUrl = args.values.url || envUrl || DASHBOARD_DEFAULT_URL;
50
+ const interval = clampDashboardMs(args.values.interval, DASHBOARD_DEFAULT_INTERVAL_MS);
51
+ const fetchTimeoutMs = clampDashboardMs(args.values.timeout, DASHBOARD_DEFAULT_TIMEOUT_MS);
52
+ const once = Boolean(args.values.once);
41
53
 
42
54
  const RESET = '\x1b[0m';
43
55
  const RED = '\x1b[31m';
@@ -73,22 +85,6 @@ function sanitizeField(value, maxLen = MAX_FIELD_LEN) {
73
85
  return text;
74
86
  }
75
87
 
76
- function siblingUrl(snapshotUrl, suffix) {
77
- try {
78
- const u = new URL(snapshotUrl);
79
- let basePath = u.pathname;
80
- if (basePath.endsWith('/snapshot')) {
81
- basePath = basePath.slice(0, -'/snapshot'.length);
82
- } else if (basePath.endsWith('/snapshot/')) {
83
- basePath = basePath.slice(0, -'/snapshot/'.length);
84
- }
85
- u.pathname = `${basePath}${suffix}`;
86
- return u.toString();
87
- } catch {
88
- return null;
89
- }
90
- }
91
-
92
88
  /**
93
89
  * @returns {{ ok: true, data: unknown } | { ok: false, kind: 'waiting' | 'failure', error?: string }}
94
90
  */
@@ -340,6 +336,7 @@ function renderHardening(snapshotResult) {
340
336
  } else {
341
337
  const detail = snapshotResult.error ? ` (${snapshotResult.error})` : '';
342
338
  console.log(`${RED}Failure contacting kernel${detail}${RESET}`);
339
+ if (!urlExplicit) console.log(`${MUTED}${DASHBOARD_EPHEMERAL_PORT_HINT}${RESET}`);
343
340
  }
344
341
  return;
345
342
  }
@@ -381,13 +378,13 @@ function renderHardening(snapshotResult) {
381
378
  }
382
379
 
383
380
  async function render() {
384
- process.stdout.write('\x1b[2J\x1b[H');
381
+ if (!once) process.stdout.write('\x1b[2J\x1b[H');
385
382
  console.log(`ArkGate Observability Dashboard`);
386
383
  console.log(`Time: ${sanitizeField(new Date().toISOString())}`);
387
384
  console.log(`Endpoint: ${sanitizeField(targetUrl)}\n`);
388
385
 
389
- const outboxUrl = siblingUrl(targetUrl, '/outbox');
390
- const workflowsUrl = siblingUrl(targetUrl, '/workflows');
386
+ const outboxUrl = dashboardSiblingUrl(targetUrl, '/outbox');
387
+ const workflowsUrl = dashboardSiblingUrl(targetUrl, '/workflows');
391
388
  const [snapshotResult, outboxResult, workflowsResult] = await Promise.all([
392
389
  fetchJsonResult(targetUrl),
393
390
  fetchJsonResult(outboxUrl),
@@ -404,9 +401,15 @@ async function render() {
404
401
  snapshot?.outbox,
405
402
  snapshot?.workflows,
406
403
  );
404
+ return snapshotResult.ok;
407
405
  }
408
406
 
409
407
  async function startDashboard() {
408
+ if (once) {
409
+ const ok = await render();
410
+ process.exitCode = ok ? 0 : 1;
411
+ return;
412
+ }
410
413
  console.log(
411
414
  `Starting ArkGate Observability Dashboard (polling every ${interval}ms, fetch timeout ${fetchTimeoutMs}ms)`,
412
415
  );