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.
- package/CHANGELOG.md +111 -0
- package/README.md +16 -4
- package/bin/ark-check-runtime.mjs +75 -3
- package/bin/ark-mcp-runtime.mjs +94 -0
- package/bin/lib/adapter-contract.mjs +14 -1
- package/bin/lib/ambient-state.mjs +64 -8
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/architecture-scan.mjs +35 -2
- package/bin/lib/arkrule-file-hints.mjs +71 -0
- package/bin/lib/arkrules-contract.mjs +382 -0
- package/bin/lib/arkrules-sensors.mjs +411 -0
- package/bin/lib/config-contract.mjs +85 -6
- package/bin/lib/doctor-advisories.mjs +22 -5
- package/bin/lib/doctor-plan.mjs +68 -13
- package/bin/lib/effective-contract-load.mjs +116 -0
- package/bin/lib/enforcement-honesty.mjs +225 -0
- package/bin/lib/field-install.mjs +104 -0
- package/bin/lib/graph-blind.mjs +254 -0
- package/bin/lib/html-report-advisories.mjs +29 -3
- package/bin/lib/install-migrate.mjs +20 -2
- package/bin/lib/invariant-coverage-io.mjs +157 -0
- package/bin/lib/invariant-coverage.mjs +127 -0
- package/bin/lib/pilot-loop.mjs +19 -0
- package/bin/lib/policy-delta-io.mjs +33 -0
- package/bin/lib/post-green-path.mjs +22 -1
- package/bin/lib/presets.mjs +241 -1
- package/bin/lib/remediation.mjs +28 -0
- package/bin/lib/resolved-candidate-facts.mjs +14 -1
- package/bin/lib/rules-inventory.mjs +144 -0
- package/bin/lib/rules-under-contract.mjs +66 -0
- package/bin/lib/start-preview.mjs +24 -7
- package/bin/lib/upgrade-command.mjs +48 -2
- package/dist/{configTypes-DAPvBqK6.d.ts → configTypes-CC0FEXoF.d.ts} +16 -3
- package/dist/eslint/index.cjs +2 -2
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +2 -2
- package/dist/index.cjs +14 -7
- package/dist/index.d.ts +615 -20
- package/dist/index.js +13 -6
- package/docs/README.md +4 -3
- package/docs/agent-guide.md +7 -3
- package/docs/ai-gates.md +6 -1
- package/docs/brownfield-adoption.md +20 -0
- package/docs/configuration.md +37 -4
- package/docs/develop.md +8 -2
- package/docs/enthusiast/README.md +11 -0
- package/docs/package-surface.md +13 -11
- package/docs/product-voice.md +9 -2
- package/docs/use.md +9 -0
- package/package.json +4 -17
- package/schemas/ark.analysis-result.schema.json +9 -1
- package/schemas/ark.arkrules.schema.json +141 -0
- package/schemas/ark.config.schema.json +10 -2
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/server.json +3 -3
- package/templates/arkrules/ApplicationOrchestration.json +14 -0
- package/templates/arkrules/DomainModel.json +32 -0
- package/templates/arkrules/PersistenceAdapters.json +14 -0
- package/templates/arkrules/PresentationAdapters.json +14 -0
- package/templates/skills/ark-adopt.md +28 -1
- package/templates/skills/ark-architect.md +23 -0
- package/templates/skills/ark-autopilot.md +27 -1
- package/templates/skills/ark-contract.md +27 -1
- package/templates/skills/ark-coverage.md +30 -0
- package/templates/skills/ark-explain.md +23 -0
- package/templates/skills/ark-explore.md +30 -2
- package/templates/skills/ark-fix.md +23 -0
- package/templates/skills/ark-loop.md +23 -0
- package/templates/skills/ark-place.md +30 -0
- package/templates/skills/ark-runtime.md +4 -0
- package/templates/skills/ark-think.md +24 -1
- package/templates/skills/ark-upgrade.md +23 -0
- package/compat/nestjs.cjs +0 -2
- package/compat/nestjs.d.ts +0 -2
- package/compat/nestjs.js +0 -1
- package/compat/runtime.cjs +0 -2
- package/compat/runtime.d.ts +0 -2
- 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
|
|
20
|
-
>
|
|
21
|
-
> [3.9.
|
|
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
|
|
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 {
|
|
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);
|
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
/**
|
|
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
|
|
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(
|
|
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
|
}
|