arkgate 3.9.1 → 4.0.0

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 (78) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +16 -4
  3. package/bin/ark-check-runtime.mjs +75 -3
  4. package/bin/ark-mcp-runtime.mjs +94 -0
  5. package/bin/lib/adapter-contract.mjs +14 -1
  6. package/bin/lib/ambient-state.mjs +64 -8
  7. package/bin/lib/analysis-engine.mjs +8 -8
  8. package/bin/lib/architecture-scan.mjs +35 -2
  9. package/bin/lib/arkrule-file-hints.mjs +71 -0
  10. package/bin/lib/arkrules-contract.mjs +382 -0
  11. package/bin/lib/arkrules-sensors.mjs +411 -0
  12. package/bin/lib/config-contract.mjs +85 -6
  13. package/bin/lib/doctor-advisories.mjs +22 -5
  14. package/bin/lib/doctor-plan.mjs +68 -13
  15. package/bin/lib/effective-contract-load.mjs +116 -0
  16. package/bin/lib/enforcement-honesty.mjs +225 -0
  17. package/bin/lib/field-install.mjs +104 -0
  18. package/bin/lib/graph-blind.mjs +254 -0
  19. package/bin/lib/html-report-advisories.mjs +29 -3
  20. package/bin/lib/install-migrate.mjs +20 -2
  21. package/bin/lib/invariant-coverage-io.mjs +157 -0
  22. package/bin/lib/invariant-coverage.mjs +127 -0
  23. package/bin/lib/pilot-loop.mjs +19 -0
  24. package/bin/lib/policy-delta-io.mjs +33 -0
  25. package/bin/lib/post-green-path.mjs +22 -1
  26. package/bin/lib/presets.mjs +241 -1
  27. package/bin/lib/remediation.mjs +28 -0
  28. package/bin/lib/resolved-candidate-facts.mjs +14 -1
  29. package/bin/lib/rules-inventory.mjs +144 -0
  30. package/bin/lib/rules-under-contract.mjs +66 -0
  31. package/bin/lib/start-preview.mjs +24 -7
  32. package/bin/lib/upgrade-command.mjs +48 -2
  33. package/dist/{configTypes-DAPvBqK6.d.ts → configTypes-CC0FEXoF.d.ts} +16 -3
  34. package/dist/eslint/index.cjs +2 -2
  35. package/dist/eslint/index.d.ts +1 -1
  36. package/dist/eslint/index.js +2 -2
  37. package/dist/index.cjs +14 -7
  38. package/dist/index.d.ts +615 -20
  39. package/dist/index.js +13 -6
  40. package/docs/README.md +4 -3
  41. package/docs/agent-guide.md +7 -3
  42. package/docs/ai-gates.md +6 -1
  43. package/docs/brownfield-adoption.md +20 -0
  44. package/docs/configuration.md +37 -4
  45. package/docs/develop.md +8 -2
  46. package/docs/enthusiast/README.md +11 -0
  47. package/docs/package-surface.md +13 -11
  48. package/docs/product-voice.md +9 -2
  49. package/docs/use.md +9 -0
  50. package/package.json +4 -17
  51. package/schemas/ark.analysis-result.schema.json +9 -1
  52. package/schemas/ark.arkrules.schema.json +141 -0
  53. package/schemas/ark.config.schema.json +10 -2
  54. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  55. package/server.json +3 -3
  56. package/templates/arkrules/ApplicationOrchestration.json +14 -0
  57. package/templates/arkrules/DomainModel.json +32 -0
  58. package/templates/arkrules/PersistenceAdapters.json +14 -0
  59. package/templates/arkrules/PresentationAdapters.json +14 -0
  60. package/templates/skills/ark-adopt.md +28 -1
  61. package/templates/skills/ark-architect.md +23 -0
  62. package/templates/skills/ark-autopilot.md +27 -1
  63. package/templates/skills/ark-contract.md +27 -1
  64. package/templates/skills/ark-coverage.md +30 -0
  65. package/templates/skills/ark-explain.md +23 -0
  66. package/templates/skills/ark-explore.md +30 -2
  67. package/templates/skills/ark-fix.md +23 -0
  68. package/templates/skills/ark-loop.md +23 -0
  69. package/templates/skills/ark-place.md +30 -0
  70. package/templates/skills/ark-runtime.md +4 -0
  71. package/templates/skills/ark-think.md +24 -1
  72. package/templates/skills/ark-upgrade.md +23 -0
  73. package/compat/nestjs.cjs +0 -2
  74. package/compat/nestjs.d.ts +0 -2
  75. package/compat/nestjs.js +0 -1
  76. package/compat/runtime.cjs +0 -2
  77. package/compat/runtime.d.ts +0 -2
  78. package/compat/runtime.js +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,117 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.0.0 — 2026-07-24
9
+
10
+ **Major** over 3.9.2. **Breaking:** deprecated root subpaths `arkgate/runtime` and `arkgate/nestjs`
11
+ are removed (use `@arkgate/runtime`). **ArkRules (opt-in):** intra-layer structural sensors,
12
+ invariant catalogs, coverage evidence, brownfield rules inventory, and modular `arkrules/*.json`
13
+ on the same enforcement plane (CLI, MCP, PreToolUse, CI, doctor). Absence of `arkRules` changes no
14
+ inter-layer verdict. **Not field-cohort proven:** Z09/RB-11 retained adoption remains open; AR16
15
+ case-study docs are scaffolding, not a closed field gate.
16
+
17
+ ### Breaking (AR04)
18
+
19
+ - **Removed** deprecated root package subpaths `arkgate/runtime` and `arkgate/nestjs`
20
+ (and the `compat/` forwarders). Import `@arkgate/runtime` / `@arkgate/runtime/nestjs`
21
+ instead. See [migration note](docs/migrate-from-ark-runtime-kernel.md#arkgate-4--ar04--root-runtime-forwarders-removed).
22
+
23
+ ### Added — ArkRules foundations + sensors (AR01–AR08)
24
+
25
+ - **AR01 — ArkRules foundations (ADR 0012):** optional `arkRules` map on `ark.config.json`
26
+ (`schemaVersion` `1.0→1.1` additive migration), sibling schema
27
+ `schemas/ark.arkrules.schema.json` (`arkgate/schema/arkrules`), pure
28
+ `loadArkRulesContract` / `resolveEffectiveContract` with per-rule provenance, and fail-closed
29
+ diagnostics for missing/invalid referenced files. Absence of `arkRules` changes no inter-layer
30
+ verdict. Zero-match `appliesTo` emits `ARKRULE_SCOPE_EMPTY` (advisory warn / enforced fail).
31
+ - **AR02 — Effective Contract policyHash + policy-delta:** `loadContract` folds non-empty
32
+ ArkRules into `policyHash` (absent → historical hash preserved); policy-delta classifies
33
+ arkrule add/remove/promote/demote; CLI loads referenced files via
34
+ `bin/lib/effective-contract-load.mjs` and observes them for resident invalidation.
35
+ - **AR03 — Adapter contract 1.4:** diagnostics carry optional `evidence.arkruleId` +
36
+ `evidence.arkruleSource`; remediation/nextAction for `ARKRULE_*` / `INVARIANT_UNCOVERED`
37
+ is judgment-only with provenance on every surface.
38
+ - **ADR 0013** + resolved facts schema `1.1` optional `classShapes[]` (class-shape evidence)
39
+ produced on the write/CI path via `extractClassShapesFromSource`.
40
+ - **Tier-1 sensors:** `aggregate-private-state`, `always-valid-factory`,
41
+ `domain-event-on-mutation`, `orchestration-only`, `thin-adapter`.
42
+ - **Tier-2 advisory:** `no-anemic-model` (never promotable).
43
+ - **fileHints:** Tooling derives conservative `orchestrationHeavy` / `adapterThick` hints
44
+ (`deriveArkRuleFileHints`) and feeds the write/CI scan path (prefer false negatives;
45
+ default templates stay advisory).
46
+ - **Templates:** `templates/arkrules/*.json` + presets/init emit lean `arkRules` refs and
47
+ copy editable starter files.
48
+
49
+ ### Added — invariants, inventory, migration surfaces (shipped in package; progressive opt-in)
50
+
51
+ - **ADR 0014–0016:** invariant catalog + coverage evidence + promotion ladder; migration
52
+ routes through existing skills; no executable evaluator in core.
53
+ - **Invariant coverage:** test-title + symbol evidence; `INVARIANT_UNCOVERED`; partial when
54
+ test globs missing; `canPromoteInvariant` refuses uncovered promotions. Write path loads real
55
+ test contents via `bin/lib/invariant-coverage-io.mjs`. CLI policy-delta loads Effective ArkRules
56
+ + coverage so covered promotions can auto-strengthen.
57
+ - **Doctor/HTML** `rulesUnderContract` (counts, not a score); report parity key.
58
+ - **Rules inventory:** `ark-check --rules-inventory` + MCP `ark_rules_inventory`; extraction
59
+ cards for pilotLoop; freeze residual reuses baseline keys.
60
+ - Skills deepen (`ark-adopt`, `ark-contract`, `ark-place`) without new skill names.
61
+ - Pre-release field dogfood workflow: `.grok/workflows/pre-release-field-dogfood.rhai`.
62
+
63
+ ### Confidence / package budgets (4.0.0)
64
+
65
+ - **Branch floor** recalibrated **84.5% → 83.0%** after ArkRules dual-plane growth (measured
66
+ ~83.3% on the clean candidate). Statement / function / line floors and mutation gates unchanged.
67
+ - **Module LOC budgets** raised for `ark-check-runtime`, `doctor-plan`, `presets`,
68
+ `html-report-advisories` (evidence in `scripts/check-module-budgets.mjs`).
69
+ - **Package pack budgets** remeasured for 4.0.0 (≥10% headroom in `release/package-budgets.v1.json`).
70
+
71
+ ### Honesty / not claimed in 4.0.0
72
+
73
+ - No claim that Z09 / RB-11 retained field adoption is closed.
74
+ - No claim that a consented multi-adopter field pilot (AR16 gate) is complete — case-study docs
75
+ are present; cohort evidence is not.
76
+ - No numeric trust score; green with frozen residual still says so.
77
+ - **Dual-truth residual:** `ark upgrade --no-install` can refresh managed assets while leaving
78
+ package.json on an older pin — doctor exposes `packageVersionTruth` and upgrade JSON/human notes
79
+ when the pin is behind the CLI.
80
+
81
+ ## 3.9.2 — 2026-07-23
82
+
83
+ **Patch** over 3.9.1. Product honesty for post-validity coaching, coverage/host write paths, and
84
+ advisory analysis precision — **no required config migration**, no gate weakening, no hard-write
85
+ claims on soft hosts. Y07 / Y09 remain **parked** (not promoted).
86
+
87
+ ### Added
88
+
89
+ - **Enforcement honesty helpers** (`bin/lib/enforcement-honesty.mjs`): coverage honesty
90
+ (empty / weak <50% worse-than-no-gate / partial / strong; `greenIsNotEnforcement` until 100%;
91
+ `wholeTreeGoverned`), baseline dirty-freeze risk, write-path honesty with soft hosts derived from
92
+ `HOST_SUPPORT_MATRIX` (fail-closed: soft never `hardWriteActive`).
93
+ - **Graph blind spots** (`bin/lib/graph-blind.mjs`): advisory scan for unresolvable dynamic
94
+ import/require edges (template-interpolation + non-literals + import-equals). Never a hard
95
+ architecture verdict; Y09 direction only.
96
+ - Doctor / plan JSON: `coverageHonesty`, `baseline.honesty`, `writePath.honesty`,
97
+ `graphBlindSpots`, design-weak honesty flags (`healthyFinishedForbidden`,
98
+ `multiPilotBatchForbidden`, `autoApplyForbidden` / `autoApplyPlanBForbidden`).
99
+ - Focused unit suite `tests/unit/static-check/enforcementHonesty.test.ts`.
100
+
101
+ ### Changed
102
+
103
+ - **Post-green path:** placement coaching + shared design-weak honesty flags.
104
+ - **Pilot loop:** one-at-a-time queue (`queuedBets` / `queueNote`); multi-pilot batch and silent
105
+ plan-B auto-apply forbidden on all return paths.
106
+ - **Ambient sensor (Y07 honesty only):** status vocabulary (`idle` / `active-clean` /
107
+ `active-findings` / `unavailable`), `blockerGrade: false`, `strictDiagnostics: 'parked-Y07'`;
108
+ idle/clean/unavailable print honesty lines (not silence-as-done).
109
+ - **Skills** (`ark-coverage`, `ark-explore`, `ark-place`): deepen honesty / one-pilot routing
110
+ without new skill basenames.
111
+ - **HTML advisories:** graphBlindSpots X01 parity; ambient h2 parked-Y07 wording.
112
+
113
+ ### Notes
114
+
115
+ - Soft write hosts (Cursor / Codex / OpenCode) remain advisory at write; required CI status is the
116
+ hard merge boundary.
117
+ - Z09 / residual `RB-11` remain open. Y07 / Y09 not marked done.
118
+
8
119
  ## 3.9.1 — 2026-07-23
9
120
 
10
121
  **Patch** over 3.9.0. Repo hygiene and CI honesty only — **no required config migration**, no gate
package/README.md CHANGED
@@ -16,9 +16,9 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 3.9.1** is the next prepared patch over **3.9.0** (onboarding/CI lockfile honesty,
20
- > supply-chain hygiene). **npm `latest` is still 3.9.0** until publication succeeds.
21
- > [3.9.1 notes](docs/releases/3.9.1.md) · [3.9.0](docs/releases/3.9.0.md) · [Docs hub](docs/README.md)
19
+ > **ArkGate 4.0.0** is prepared on this line (ArkRules opt-in + AR04 breaking forwarder removal).
20
+ > Last npm `latest` remains **3.9.2** until publish. [4.0.0 notes](docs/releases/4.0.0.md) ·
21
+ > [3.9.2](docs/releases/3.9.2.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
22
22
 
23
23
  ---
24
24
 
@@ -65,6 +65,16 @@ A machine-readable architecture file (`ark.config.json`) plus enforcement:
65
65
  | **While the AI writes** | Hard PreToolUse on supported hosts; advisory MCP elsewhere |
66
66
  | **Before merge** | `arkgate-check` as a **required** CI status |
67
67
 
68
+ ### Two planes (4.0)
69
+
70
+ | Plane | What it guards | Config |
71
+ |-------|----------------|--------|
72
+ | **Layers** (always) | Who may talk to whom — imports, placement, purity, isolation | `ark.config.json` layers + rules |
73
+ | **ArkRules** (opt-in) | Habits *inside* a layer — structure sensors + domain invariants as data | `arkRules` → `arkrules/<Layer>.json` |
74
+
75
+ Absence of ArkRules changes no inter-layer verdict. Label residual **`[Layer]`** vs **`[ArkRules]`**.
76
+ Details: [configuration](docs/configuration.md#arkrules-intra-layer-opt-in) · [use path](docs/use.md).
77
+
68
78
  **Not** a web framework, ORM, or job runner. Optional experimental runtime is separate and not required for the gate.
69
79
 
70
80
  **Name note:** npm package `arkgate` — not affiliated with the separate Archgate CLI project.
@@ -131,6 +141,7 @@ Setup per host: [docs/ai-gates.md](docs/ai-gates.md) · Develop path: [docs/deve
131
141
  | Contract agents can read (`ark://manifest`) | ✅ | ❌ |
132
142
  | Placement + preflight for multi-file changes | ✅ | ❌ |
133
143
  | Honest governed % + dual plan (edges vs shape) | ✅ | ❌ |
144
+ | Opt-in intra-layer ArkRules (structure + invariants) | ✅ | ❌ |
134
145
  | Incomplete analysis cannot look green | ✅ | varies |
135
146
 
136
147
  ---
@@ -175,7 +186,8 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
175
186
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
176
187
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
177
188
  | Security | [SECURITY.md](SECURITY.md) |
178
- | Latest release (3.9.0 on npm; 3.9.1 prepared) | [3.9.1](docs/releases/3.9.1.md) · [3.9.0](docs/releases/3.9.0.md) · [CHANGELOG](CHANGELOG.md) |
189
+ | Latest prepared (4.0.0) | [docs/releases/4.0.0.md](docs/releases/4.0.0.md) · [CHANGELOG](CHANGELOG.md) |
190
+ | Last published (3.9.2) | [docs/releases/3.9.2.md](docs/releases/3.9.2.md) |
179
191
  | History / maintainer evidence | [docs/archive/](docs/archive/README.md) |
180
192
 
181
193
  ---
@@ -84,7 +84,11 @@ import {
84
84
  detectBestFitModel,
85
85
  dirSegmentsFromGlob,
86
86
  } from './lib/suggestions.mjs';
87
- import { ARCHITECTURE_PRESETS } from './lib/presets.mjs';
87
+ import {
88
+ ARCHITECTURE_PRESETS,
89
+ withDefaultArkRules,
90
+ writeArkRulesTemplates,
91
+ } from './lib/presets.mjs';
88
92
  import { loadArkConfigContract, parseArkConfigJson } from './lib/config-contract.mjs';
89
93
  import { createAdapterResult } from './lib/adapter-contract.mjs';
90
94
  import { collectGovernedFiles, normalize, walk } from './lib/scan-files.mjs';
@@ -180,6 +184,7 @@ function parseArgs(argv) {
180
184
  else if (arg === '--coverage') args.coverage = true;
181
185
  else if (arg === '--doctor') args.doctor = true;
182
186
  else if (arg === '--plan') args.plan = true;
187
+ else if (arg === '--rules-inventory') args.rulesInventory = true;
183
188
  else if (arg === '--recommend') args.recommend = true;
184
189
  else if (arg === '--write-plan') args.writePlan = true;
185
190
  else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
@@ -239,6 +244,7 @@ function usage() {
239
244
  ' ark-check --doctor [--json] [--resident] [--fail-on-new-smells --base-ref <git-ref>] read-only diagnosis; resident JSON falls back cold',
240
245
  ' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
241
246
  ' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
247
+ ' ark-check --rules-inventory [--json] brownfield rules inventory (AR13; deterministic candidates, not a score)',
242
248
  ' ark-check --recommend [--json] [--write-plan] application-shape plan; --write-plan emits ark-adoption-plan.json',
243
249
  ' ark-check --list-policy-packs enthusiast packs (hexagonal, layered, feature-sliced, monorepo, ui-surface, vertical-slice, ddd-bounded-contexts)',
244
250
  ' ark-check --apply-policy-pack <id> [--force] write ark.config.json from templates/policy-packs/ (uses preset factory)',
@@ -515,6 +521,7 @@ function runApplyPolicyPack(args) {
515
521
  try {
516
522
  const { pack, config } = buildConfigFromPolicyPack(args.applyPolicyPack, args.root);
517
523
  fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
524
+ writeArkRulesTemplates(args.root, config, { force: args.force === true });
518
525
  if (args.json) {
519
526
  console.log(
520
527
  JSON.stringify(
@@ -750,6 +757,12 @@ function runInit(args) {
750
757
  args.root
751
758
  );
752
759
  fs.writeFileSync(configPath, `${JSON.stringify(finalConfig, null, 2)}\n`);
760
+ const arkrulesWritten = writeArkRulesTemplates(args.root, finalConfig, {
761
+ force: args.force === true,
762
+ });
763
+ if (arkrulesWritten.length > 0) {
764
+ console.log(`Wrote ArkRules templates: ${arkrulesWritten.join(', ')}`);
765
+ }
753
766
  console.log(`Wrote ${configPath} (${args.preset} preset)`);
754
767
  if (finalConfig.frameworkOverlay) {
755
768
  console.log(
@@ -788,7 +801,7 @@ function runInit(args) {
788
801
  // scaffold under) even when src/ doesn't exist yet — the layers are optional, so the
789
802
  // check passes today and governance switches on the moment src/domain/ etc. appear.
790
803
  // Detected configs also get framework overlays so Nest/Next flat files are classified.
791
- const finalConfig =
804
+ const finalConfig = withDefaultArkRules(
792
805
  mode === 'detected'
793
806
  ? applyFrameworkLayoutOverlays(config, args.root)
794
807
  : mode === 'monorepo'
@@ -799,9 +812,11 @@ function runInit(args) {
799
812
  : createElevenLayerConfig({
800
813
  rootDir: srcDir === '.' ? 'src' : srcDir,
801
814
  root: args.root,
802
- });
815
+ })
816
+ );
803
817
 
804
818
  fs.writeFileSync(configPath, `${JSON.stringify(finalConfig, null, 2)}\n`);
819
+ writeArkRulesTemplates(args.root, finalConfig, { force: args.force === true });
805
820
 
806
821
  console.log(`Wrote ${configPath}`);
807
822
  console.log('');
@@ -1244,6 +1259,63 @@ async function main() {
1244
1259
  return;
1245
1260
  }
1246
1261
 
1262
+ if (args.rulesInventory) {
1263
+ const { buildRulesInventory, inventoryToExtractionCard } = await import('./lib/rules-inventory.mjs');
1264
+ const fileContents = {};
1265
+ for (const file of files.slice(0, 400)) {
1266
+ const rel = normalize(path.relative(root, file));
1267
+ try {
1268
+ fileContents[rel] = fs.readFileSync(file, 'utf8');
1269
+ } catch {
1270
+ /* skip unreadable */
1271
+ }
1272
+ }
1273
+ const contracted = [];
1274
+ if (config.arkRules) {
1275
+ try {
1276
+ const { loadEffectiveArkRulesFromDisk } = await import('./lib/effective-contract-load.mjs');
1277
+ const loaded = loadEffectiveArkRulesFromDisk(root, config);
1278
+ for (const rule of loaded.arkRules.structure ?? []) contracted.push(rule.id);
1279
+ for (const inv of loaded.arkRules.invariants ?? []) contracted.push(inv.id);
1280
+ } catch {
1281
+ /* advisory inventory still useful */
1282
+ }
1283
+ }
1284
+ const inventory = buildRulesInventory({
1285
+ fileContents,
1286
+ contractedRuleIds: contracted,
1287
+ });
1288
+ const nextPilot =
1289
+ inventory.candidates[0] != null
1290
+ ? inventoryToExtractionCard(inventory.candidates[0])
1291
+ : null;
1292
+ const payload = {
1293
+ rulesInventory: inventory,
1294
+ rulesMigration: {
1295
+ inventoried: inventory.inventoried,
1296
+ underContract: inventory.underContract,
1297
+ frozen: inventory.frozen,
1298
+ notAScore: true,
1299
+ },
1300
+ nextPilot: nextPilot,
1301
+ };
1302
+ if (args.json) {
1303
+ console.log(JSON.stringify(payload, null, 2));
1304
+ } else {
1305
+ console.log(
1306
+ `Rules inventory: ${inventory.inventoried} inventoried, ${inventory.underContract} under contract, ${inventory.frozen} frozen (not a score).`
1307
+ );
1308
+ for (const c of inventory.candidates.slice(0, 12)) {
1309
+ console.log(` - [${c.confidence}] ${c.kind} @ ${c.file}:${c.line} — ${c.message}`);
1310
+ }
1311
+ if (nextPilot) {
1312
+ console.log(`Next extraction pilot: ${nextPilot.pilot} → ${nextPilot.pilotTarget}`);
1313
+ }
1314
+ }
1315
+ process.exitCode = 0;
1316
+ return;
1317
+ }
1318
+
1247
1319
  const skillGaps = detectSkillGaps(root);
1248
1320
  const codexHomeGap = detectCodexHomeGap(root);
1249
1321
  const codexRepoSkillGap = detectCodexRepoSkillGap(root);
@@ -22,7 +22,40 @@ import { createImportTargetResolver } from './lib/import-resolve.mjs';
22
22
  import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
23
23
  import { composePrepareWrite } from './lib/prepare-write.mjs';
24
24
  import { loadArkConfigContract } from './lib/config-contract.mjs';
25
+ import { loadEffectiveArkRulesFromDisk } from './lib/effective-contract-load.mjs';
26
+ import {
27
+ buildRulesInventory,
28
+ inventoryToExtractionCard,
29
+ } from './lib/rules-inventory.mjs';
25
30
  import { ARK_ANALYSIS_RESULT_SCHEMA, createAdapterResult } from './lib/adapter-contract.mjs';
31
+
32
+ function arkRulesCatalogForManifest(root, config) {
33
+ if (!config?.arkRules || typeof config.arkRules !== 'object') return {};
34
+ try {
35
+ const loaded = loadEffectiveArkRulesFromDisk(root, config);
36
+ if (loaded.errors?.length || !loaded.arkRules) return {};
37
+ const structure = (loaded.arkRules.structure ?? []).map((r) => ({
38
+ id: r.id,
39
+ sensor: r.sensor,
40
+ mode: r.mode,
41
+ layer: r.provenance?.layer,
42
+ sourceFile: r.provenance?.sourceFile,
43
+ }));
44
+ const invariants = (loaded.arkRules.invariants ?? []).map((r) => ({
45
+ id: r.id,
46
+ description: r.description,
47
+ aggregate: r.aggregate,
48
+ mode: r.mode,
49
+ layer: r.provenance?.layer,
50
+ sourceFile: r.provenance?.sourceFile,
51
+ coverage: r.coverage,
52
+ }));
53
+ if (structure.length === 0 && invariants.length === 0) return {};
54
+ return { arkRulesCatalog: { structure, invariants } };
55
+ } catch {
56
+ return {};
57
+ }
58
+ }
26
59
  import { loadTypeScript } from './lib/typescript-host.mjs';
27
60
  import { validateSnippetAnalysis } from './lib/snippet-analysis.mjs';
28
61
  import { loadGoldenPattern, attachGoldenToPlacement } from './lib/golden-pattern.mjs';
@@ -1559,6 +1592,14 @@ export async function runArkMcp({ hookInput } = {}) {
1559
1592
  'or the contract misses package roots.',
1560
1593
  inputSchema: { type: 'object', properties: {} },
1561
1594
  },
1595
+ {
1596
+ name: 'ark_rules_inventory',
1597
+ description:
1598
+ 'Deterministic brownfield rules inventory (AR13): validation-in-controller, magic constants, ' +
1599
+ 'anemic entities, mutation-without-guard. Returns honest counts (inventoried/under-contract/frozen) ' +
1600
+ '— never a numeric score. Same plane as ark-check --rules-inventory.',
1601
+ inputSchema: { type: 'object', properties: {} },
1602
+ },
1562
1603
  ];
1563
1604
 
1564
1605
  const RESOURCES = [
@@ -1633,6 +1674,11 @@ export async function runArkMcp({ hookInput } = {}) {
1633
1674
  ...(config.safety && typeof config.safety === 'object'
1634
1675
  ? { safety: config.safety }
1635
1676
  : {}),
1677
+ // AR09 — expose ArkRules references + catalog when configured (ADR 0014).
1678
+ ...(config.arkRules && typeof config.arkRules === 'object'
1679
+ ? { arkRules: config.arkRules }
1680
+ : {}),
1681
+ ...arkRulesCatalogForManifest(args.root, config),
1636
1682
  ...(suggestions.length > 0
1637
1683
  ? {
1638
1684
  suggestedLayers: suggestions,
@@ -1981,6 +2027,53 @@ export async function runArkMcp({ hookInput } = {}) {
1981
2027
  }
1982
2028
  }
1983
2029
 
2030
+ function runRulesInventoryTool() {
2031
+ try {
2032
+ const governed = collectGovernedFiles(args.root, config);
2033
+ const fileContents = {};
2034
+ for (const file of governed.slice(0, 400)) {
2035
+ const rel = path.relative(args.root, file).split(path.sep).join('/');
2036
+ try {
2037
+ fileContents[rel] = fs.readFileSync(file, 'utf8');
2038
+ } catch {
2039
+ /* skip */
2040
+ }
2041
+ }
2042
+ const contracted = [];
2043
+ const loaded = loadEffectiveArkRulesFromDisk(args.root, config);
2044
+ for (const rule of loaded.arkRules?.structure ?? []) contracted.push(rule.id);
2045
+ for (const inv of loaded.arkRules?.invariants ?? []) contracted.push(inv.id);
2046
+ const inventory = buildRulesInventory({
2047
+ fileContents,
2048
+ contractedRuleIds: contracted,
2049
+ });
2050
+ const nextPilot =
2051
+ inventory.candidates[0] != null
2052
+ ? inventoryToExtractionCard(inventory.candidates[0])
2053
+ : null;
2054
+ const payload = {
2055
+ ok: true,
2056
+ rulesInventory: inventory,
2057
+ rulesMigration: {
2058
+ inventoried: inventory.inventoried,
2059
+ underContract: inventory.underContract,
2060
+ frozen: inventory.frozen,
2061
+ notAScore: true,
2062
+ },
2063
+ nextPilot,
2064
+ };
2065
+ return {
2066
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
2067
+ structuredContent: payload,
2068
+ };
2069
+ } catch (error) {
2070
+ return {
2071
+ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
2072
+ isError: true,
2073
+ };
2074
+ }
2075
+ }
2076
+
1984
2077
  function runSuggestIncludeTool() {
1985
2078
  try {
1986
2079
  const workspaces = detectWorkspaces(args.root);
@@ -2028,6 +2121,7 @@ export async function runArkMcp({ hookInput } = {}) {
2028
2121
  ark_prepare_change: runPrepareChange,
2029
2122
  ark_recommend: runRecommendTool,
2030
2123
  ark_suggest_include: runSuggestIncludeTool,
2124
+ ark_rules_inventory: runRulesInventoryTool,
2031
2125
  };
2032
2126
 
2033
2127
  const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`);
@@ -8,7 +8,8 @@
8
8
  * Pure CLI helper (bin/lib/adapter-contract.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- export const ARK_ANALYSIS_RESULT_SCHEMA_VERSION = '1.3';
11
+ /** 1.4 adds optional evidence.arkruleId + evidence.arkruleSource (ADR 0012 / AR03). */
12
+ export const ARK_ANALYSIS_RESULT_SCHEMA_VERSION = '1.4';
12
13
  function text(value) {
13
14
  return typeof value === 'string' && value.length > 0 ? value : undefined;
14
15
  }
@@ -40,6 +41,14 @@ function nextActionForDiagnostic(ruleId, evidence, violation) {
40
41
  return 'Publish through a registered intent creator, then run Ark again.';
41
42
  if (ruleId === 'PUBLISH_MISSING_SOURCE')
42
43
  return 'Add metadata.source to the publish call, then run Ark again.';
44
+ if (ruleId === 'ARKRULE_STRUCTURE' ||
45
+ ruleId === 'ARKRULE_INVARIANT' ||
46
+ ruleId === 'INVARIANT_UNCOVERED' ||
47
+ ruleId.startsWith('ARKRULE_')) {
48
+ const source = evidence.arkruleSource ?? 'arkrules/<Layer>.json';
49
+ const id = evidence.arkruleId ?? 'the ArkRule';
50
+ return `Fix the structure or invariant for ${id} (declared in ${source}), then preflight again. Do not demote the rule without a hash-bound policy acknowledgement.`;
51
+ }
43
52
  return `Resolve ${ruleId} without weakening ark.config.json, then run Ark again.`;
44
53
  }
45
54
  export function toAdapterDiagnostic(violation, fallbackSeverity = 'error') {
@@ -67,6 +76,8 @@ export function toAdapterDiagnostic(violation, fallbackSeverity = 'error') {
67
76
  : {}),
68
77
  ...(text(violation.capability) ? { capability: text(violation.capability) } : {}),
69
78
  ...(text(violation.edgeKind) ? { edgeKind: text(violation.edgeKind) } : {}),
79
+ ...(text(violation.arkruleId) ? { arkruleId: text(violation.arkruleId) } : {}),
80
+ ...(text(violation.arkruleSource) ? { arkruleSource: text(violation.arkruleSource) } : {}),
70
81
  };
71
82
  return {
72
83
  ruleId,
@@ -264,6 +275,8 @@ export const ARK_ANALYSIS_RESULT_SCHEMA = {
264
275
  peerIsolation: { type: 'boolean' },
265
276
  capability: { type: 'string', minLength: 1 },
266
277
  edgeKind: { type: 'string', minLength: 1 },
278
+ arkruleId: { type: 'string', minLength: 1 },
279
+ arkruleSource: { type: 'string', minLength: 1 },
267
280
  },
268
281
  },
269
282
  nextAction: { type: 'string', minLength: 1 },
@@ -160,19 +160,37 @@ export function detectAmbientState(ts, root, config, files, ackState = { exists:
160
160
  };
161
161
  }
162
162
 
163
+ /**
164
+ * Status vocabulary for ambient sensor honesty (Y07 strict stays parked).
165
+ * @param {{ active?: boolean, findingCount?: number }} result
166
+ */
167
+ export function ambientSensorStatus(result) {
168
+ if (!result?.active) return 'idle';
169
+ return (result.findingCount ?? 0) > 0 ? 'active-findings' : 'active-clean';
170
+ }
171
+
163
172
  /** JSON summary for doctor. Advisory only — never a verdict input. */
164
173
  export function summarizeAmbientState(result, ackState = { exists: false, acks: [] }) {
174
+ const findingCount = result.findings.length;
175
+ const status = ambientSensorStatus({ active: result.active, findingCount });
165
176
  return {
166
177
  available: true,
167
178
  active: result.active,
179
+ status,
168
180
  advisory: true,
169
- findingCount: result.findings.length,
181
+ // Strict (blocker-grade) ambient diagnostics remain parked (Y07) until a real pure corpus.
182
+ blockerGrade: false,
183
+ strictDiagnostics: 'parked-Y07',
184
+ findingCount,
170
185
  acknowledged: ackState?.invalid ? 0 : result.acknowledgedCount,
171
186
  ...(result.truncated > 0 ? { truncated: result.truncated } : {}),
172
187
  ...(result.skippedFiles > 0 ? { skippedFiles: result.skippedFiles } : {}),
173
- note: result.active
174
- ? 'Module-scope mutable state in pure layers — advisory only; acknowledge deliberate registries in the sidecar or move the state behind a port.'
175
- : 'No pure: true layer opted in; the sensor is idle.',
188
+ note:
189
+ status === 'idle'
190
+ ? 'Idle: no pure: true layer opted in. Sensor stays advisory; blocker-grade ambient diagnostics are parked (Y07) until a real pure-layer field corpus exists. Opt in via layer pure: true when ready.'
191
+ : status === 'active-findings'
192
+ ? 'Module-scope mutable state in pure layers — advisory only (never a hard verdict). Acknowledge deliberate registries in the sidecar or move state behind a port. Strict diagnostics remain parked (Y07).'
193
+ : 'Pure layers opted in; no module-scope let/var findings in the MVP envelope. Advisory sensor only — not a pass for blocker-grade ambient enforcement (Y07 parked).',
176
194
  ackFile: {
177
195
  path: ackState?.path ?? AMBIENT_STATE_ACKS_PATH,
178
196
  present: ackState?.exists === true,
@@ -188,11 +206,14 @@ export function computeAmbientState(ts, root, config, files) {
188
206
  return {
189
207
  available: false,
190
208
  active: false,
209
+ status: 'unavailable',
191
210
  advisory: true,
211
+ blockerGrade: false,
212
+ strictDiagnostics: 'parked-Y07',
192
213
  findings: [],
193
214
  findingCount: 0,
194
215
  acknowledged: 0,
195
- note: 'TypeScript was not available to the doctor run; the ambient-state sensor did not execute.',
216
+ note: 'TypeScript was not available to the doctor run; the ambient-state sensor did not execute. Advisory only; Y07 strict remains parked.',
196
217
  };
197
218
  }
198
219
  const ackState = loadAmbientStateAcks(root);
@@ -200,9 +221,41 @@ export function computeAmbientState(ts, root, config, files) {
200
221
  return { ...summarizeAmbientState(result, ackState), findings: result.findings };
201
222
  }
202
223
 
203
- /** Human doctor section (advisory); silent when idle and healthy. */
224
+ /**
225
+ * Human doctor section (advisory).
226
+ * Idle prints a single dim honesty line so silence is not misread as "ambient done."
227
+ */
204
228
  export function printAmbientStateSection(state, io) {
205
- if (!state.available || (!state.findingCount && !state.ackFile?.invalid)) return;
229
+ if (!state) return;
230
+ if (!state.available) {
231
+ console.log('');
232
+ console.log(io.color.bold('Ambient state (advisory)'));
233
+ io.line(' ', io.color.dim(state.note || 'Ambient sensor unavailable.'));
234
+ return;
235
+ }
236
+ if (state.status === 'idle' && !state.ackFile?.invalid) {
237
+ console.log('');
238
+ console.log(io.color.bold('Ambient state (advisory)'));
239
+ io.line(
240
+ ' ',
241
+ io.color.dim(
242
+ 'Idle (no pure: true layer) — advisory sensor only; blocker-grade ambient diagnostics parked (Y07).'
243
+ )
244
+ );
245
+ return;
246
+ }
247
+ if (state.status === 'active-clean' && !state.ackFile?.invalid && !state.findingCount) {
248
+ console.log('');
249
+ console.log(io.color.bold('Ambient state (advisory)'));
250
+ io.line(
251
+ ' ',
252
+ io.color.dim(
253
+ 'Pure layers clean under MVP envelope — still advisory; not Y07 blocker-grade pass.'
254
+ )
255
+ );
256
+ return;
257
+ }
258
+ if (!state.findingCount && !state.ackFile?.invalid) return;
206
259
  console.log('');
207
260
  console.log(io.color.bold('Ambient state (advisory)'));
208
261
  if (state.ackFile?.invalid) {
@@ -217,5 +270,8 @@ export function printAmbientStateSection(state, io) {
217
270
  if (state.acknowledged > 0) {
218
271
  io.line(' ', io.color.dim(`acknowledged module state: ${state.acknowledged}`));
219
272
  }
220
- io.line(' ', io.color.dim('advisory only — never blocks; move state behind a port or acknowledge it'));
273
+ io.line(
274
+ ' ',
275
+ io.color.dim('advisory only — never blocks; move state behind a port or acknowledge it (Y07 strict parked)')
276
+ );
221
277
  }