arkgate 4.8.23 → 4.8.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +5 -1
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tooling adapter: load Effective Contract from disk for a root config.
|
|
3
3
|
* Pure resolution lives in Domain (`resolveEffectiveContract`); this module owns I/O.
|
|
4
|
+
*
|
|
5
|
+
* `loadEffectiveArkRules` is the source-agnostic core (working tree, a policy base
|
|
6
|
+
* ref, or supplied file objects all pass the same path validation); only the
|
|
7
|
+
* reader differs.
|
|
4
8
|
*/
|
|
5
9
|
import fs from 'node:fs';
|
|
6
10
|
import path from 'node:path';
|
|
7
11
|
import {
|
|
8
12
|
emptyEffectiveArkRules,
|
|
9
13
|
buildEffectiveArkRules,
|
|
14
|
+
duplicateArkRuleIds,
|
|
10
15
|
loadArkRulesContract,
|
|
16
|
+
sliceScopeEscapes,
|
|
11
17
|
} from './arkrules-contract.mjs';
|
|
18
|
+
import { resolveSliceRulePlan } from '../ark-layer-match.mjs';
|
|
19
|
+
import { collectGovernedFiles } from './scan-files.mjs';
|
|
12
20
|
|
|
13
|
-
function normalizeProjectRelativePath(value) {
|
|
21
|
+
export function normalizeProjectRelativePath(value) {
|
|
14
22
|
const normalized = value.replace(/\\/g, '/');
|
|
15
23
|
if (
|
|
16
24
|
!normalized ||
|
|
@@ -40,113 +48,212 @@ function isWithinRoot(root, candidate) {
|
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
|
-
* @
|
|
51
|
+
* @typedef {{ ok: true, content: string } | { ok: false, missing?: boolean, message: string }} ArkRulesRead
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Core loader shared by every source.
|
|
56
|
+
*
|
|
44
57
|
* @param {Record<string, unknown>} config loaded ark.config.json object
|
|
45
|
-
* @param {
|
|
46
|
-
* @
|
|
58
|
+
* @param {(rel: string) => ArkRulesRead} readRelative
|
|
59
|
+
* @param {{ missingAsEmpty?: boolean }} [opts] missingAsEmpty: a referenced file absent
|
|
60
|
+
* from the source counts as an empty layer (a base that predates the file), not an error.
|
|
61
|
+
* @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, errors: Array<{path:string,message:string}>, referenced: Set<string> }}
|
|
47
62
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
function declaredLayerNames(config) {
|
|
64
|
+
return new Set(Array.isArray(config?.layers) ? config.layers.map((layer) => layer?.name).filter(Boolean) : []);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function arkRulesRefList(value) {
|
|
68
|
+
if (typeof value === 'string') return value.length > 0 ? [value] : null;
|
|
69
|
+
if (Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === 'string' && entry.length > 0)) {
|
|
70
|
+
return value;
|
|
52
71
|
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
function pushReadError(errors, pathKey, rel, error) {
|
|
76
|
+
errors.push({
|
|
77
|
+
path: pathKey,
|
|
78
|
+
message:
|
|
79
|
+
error instanceof Error
|
|
80
|
+
? error.message
|
|
81
|
+
: `referenced ArkRules file ${JSON.stringify(rel)} failed to load`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function loadEffectiveArkRules(config, readRelative, opts = {}) {
|
|
86
|
+
const refs = config?.arkRules;
|
|
87
|
+
const referenced = new Set();
|
|
88
|
+
const layerNames = declaredLayerNames(config);
|
|
57
89
|
const errors = [];
|
|
58
|
-
const warnings = [];
|
|
59
90
|
const parts = [];
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
const hasRefs = Boolean(refs && typeof refs === 'object' && Object.keys(refs).length > 0);
|
|
92
|
+
if (hasRefs) {
|
|
93
|
+
for (const layer of Object.keys(refs).sort()) {
|
|
94
|
+
const raw = refs[layer];
|
|
95
|
+
const paths = arkRulesRefList(raw);
|
|
96
|
+
const pathKey = `$.arkRules[${JSON.stringify(layer)}]`;
|
|
97
|
+
if (!paths) {
|
|
98
|
+
errors.push({ path: pathKey, message: 'must be a non-empty path string or an array of paths' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!layerNames.has(layer)) {
|
|
102
|
+
errors.push({
|
|
103
|
+
path: pathKey,
|
|
104
|
+
message: `layer ${JSON.stringify(layer)} is not declared in layers[]`,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
paths.forEach((relRaw, index) => {
|
|
109
|
+
const itemKey = paths.length === 1 && typeof raw === 'string' ? pathKey : `${pathKey}[${index}]`;
|
|
110
|
+
const rel = normalizeProjectRelativePath(relRaw);
|
|
111
|
+
if (!rel) {
|
|
112
|
+
errors.push({
|
|
113
|
+
path: itemKey,
|
|
114
|
+
message:
|
|
115
|
+
'must be a project-relative path without absolute roots or parent-directory traversal',
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
referenced.add(rel);
|
|
120
|
+
const read = readRelative(rel);
|
|
121
|
+
if (!read.ok) {
|
|
122
|
+
if (read.missing && opts.missingAsEmpty) return;
|
|
123
|
+
errors.push({ path: itemKey, message: read.message });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const loaded = loadArkRulesContract(JSON.parse(read.content), rel, layer);
|
|
128
|
+
parts.push({ layer, sourceFile: rel, file: loaded.config });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
pushReadError(errors, itemKey, rel, error);
|
|
131
|
+
}
|
|
76
132
|
});
|
|
77
|
-
continue;
|
|
78
133
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
}
|
|
135
|
+
const plan = resolveSliceRulePlan({
|
|
136
|
+
files: Array.isArray(opts.files) ? opts.files : [],
|
|
137
|
+
rules: Array.isArray(config?.rules) ? config.rules : [],
|
|
138
|
+
layers: [...layerNames],
|
|
139
|
+
});
|
|
140
|
+
for (const entry of plan) {
|
|
141
|
+
const read = readRelative(entry.path);
|
|
142
|
+
if (!read.ok) {
|
|
143
|
+
if (read.missing) continue;
|
|
144
|
+
errors.push({ path: entry.path, message: read.message });
|
|
84
145
|
continue;
|
|
85
146
|
}
|
|
147
|
+
referenced.add(entry.path);
|
|
148
|
+
try {
|
|
149
|
+
const loaded = loadArkRulesContract(JSON.parse(read.content), entry.path, entry.layer);
|
|
150
|
+
const part = {
|
|
151
|
+
layer: entry.layer,
|
|
152
|
+
sourceFile: entry.path,
|
|
153
|
+
file: loaded.config,
|
|
154
|
+
childId: entry.childId,
|
|
155
|
+
defaultAppliesTo: entry.defaultAppliesTo,
|
|
156
|
+
};
|
|
157
|
+
const escapes = sliceScopeEscapes(part);
|
|
158
|
+
if (escapes.length > 0) {
|
|
159
|
+
errors.push({
|
|
160
|
+
path: entry.path,
|
|
161
|
+
message: `ARKRULE_SCOPE_ESCAPES_SLICE: ${entry.path} appliesTo ${escapes
|
|
162
|
+
.map((row) => row.pattern)
|
|
163
|
+
.join(', ')} escapes ${entry.childId}`,
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
parts.push(part);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
pushReadError(errors, entry.path, entry.path, error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const dupe of duplicateArkRuleIds(parts)) {
|
|
173
|
+
errors.push({
|
|
174
|
+
path: `$.arkRules[${JSON.stringify(dupe.layer)}]`,
|
|
175
|
+
message: `ARKRULE_DUPLICATE_ID: ${dupe.id} is declared in ${dupe.sourceFiles.join(', ')}`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (!hasRefs && parts.length === 0 && errors.length === 0) {
|
|
179
|
+
return { arkRules: emptyEffectiveArkRules(), errors: [], referenced };
|
|
180
|
+
}
|
|
181
|
+
if (errors.length > 0) return { arkRules: emptyEffectiveArkRules(), errors, referenced };
|
|
182
|
+
return { arkRules: buildEffectiveArkRules(parts), errors: [], referenced };
|
|
183
|
+
}
|
|
86
184
|
|
|
87
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Directory reader: realpath + within-root guards, then readFileSync.
|
|
187
|
+
* @param {string} canonicalRoot realpath of the directory the paths are relative to
|
|
188
|
+
* @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
|
|
189
|
+
* @returns {(rel: string) => ArkRulesRead}
|
|
190
|
+
*/
|
|
191
|
+
export function directoryArkRulesReader(canonicalRoot, opts = {}) {
|
|
192
|
+
return (rel) => {
|
|
88
193
|
const lexicalTarget = path.resolve(canonicalRoot, ...rel.split('/'));
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
194
|
+
const outside = {
|
|
195
|
+
ok: false,
|
|
196
|
+
message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
|
|
197
|
+
};
|
|
198
|
+
if (!isWithinRoot(canonicalRoot, lexicalTarget)) return outside;
|
|
96
199
|
if (!fs.existsSync(lexicalTarget)) {
|
|
97
|
-
|
|
98
|
-
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
missing: true,
|
|
99
203
|
message: `referenced ArkRules file ${JSON.stringify(rel)} is missing`,
|
|
100
|
-
}
|
|
101
|
-
continue;
|
|
204
|
+
};
|
|
102
205
|
}
|
|
103
206
|
let absolute;
|
|
104
207
|
try {
|
|
105
208
|
absolute = fs.realpathSync(lexicalTarget);
|
|
106
209
|
} catch (error) {
|
|
107
|
-
|
|
108
|
-
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
109
212
|
message: `referenced ArkRules file ${JSON.stringify(rel)} could not be resolved: ${
|
|
110
213
|
error instanceof Error ? error.message : String(error)
|
|
111
214
|
}`,
|
|
112
|
-
}
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (!isWithinRoot(canonicalRoot, absolute)) {
|
|
116
|
-
errors.push({
|
|
117
|
-
path: pathKey,
|
|
118
|
-
message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
|
|
119
|
-
});
|
|
120
|
-
continue;
|
|
215
|
+
};
|
|
121
216
|
}
|
|
217
|
+
if (!isWithinRoot(canonicalRoot, absolute)) return outside;
|
|
122
218
|
opts.observeInput?.(absolute, 'arkrules');
|
|
123
|
-
let content;
|
|
124
219
|
try {
|
|
125
|
-
content
|
|
220
|
+
return { ok: true, content: fs.readFileSync(absolute, 'utf8') };
|
|
126
221
|
} catch (error) {
|
|
127
|
-
|
|
128
|
-
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
129
224
|
message: `referenced ArkRules file ${JSON.stringify(rel)} could not be read: ${
|
|
130
225
|
error instanceof Error ? error.message : String(error)
|
|
131
226
|
}`,
|
|
132
|
-
}
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
const loaded = loadArkRulesContract(JSON.parse(content), rel, layer);
|
|
137
|
-
parts.push({ layer, sourceFile: rel, file: loaded.config });
|
|
138
|
-
} catch (error) {
|
|
139
|
-
errors.push({
|
|
140
|
-
path: pathKey,
|
|
141
|
-
message:
|
|
142
|
-
error instanceof Error
|
|
143
|
-
? error.message
|
|
144
|
-
: `referenced ArkRules file ${JSON.stringify(rel)} failed to load`,
|
|
145
|
-
});
|
|
227
|
+
};
|
|
146
228
|
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Reader over supplied ArkRules file objects keyed by project-relative path
|
|
234
|
+
* (MCP callers hand a catalog as data instead of a checkout).
|
|
235
|
+
* @param {Record<string, unknown>} files
|
|
236
|
+
* @returns {(rel: string) => ArkRulesRead}
|
|
237
|
+
*/
|
|
238
|
+
export function objectArkRulesReader(files) {
|
|
239
|
+
const byPath = new Map();
|
|
240
|
+
for (const [key, value] of Object.entries(files ?? {})) {
|
|
241
|
+
const rel = normalizeProjectRelativePath(String(key));
|
|
242
|
+
if (rel) byPath.set(rel, value);
|
|
147
243
|
}
|
|
244
|
+
return (rel) =>
|
|
245
|
+
byPath.has(rel)
|
|
246
|
+
? { ok: true, content: JSON.stringify(byPath.get(rel)) }
|
|
247
|
+
: {
|
|
248
|
+
ok: false,
|
|
249
|
+
missing: true,
|
|
250
|
+
message: `ArkRules file ${JSON.stringify(rel)} was not supplied`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
148
253
|
|
|
149
|
-
|
|
254
|
+
/** Advisory drift: `arkrules/*.json` files the arkRules map does not reference (ADR 0012 D2). */
|
|
255
|
+
function unreferencedArkRulesWarnings(canonicalRoot, referenced) {
|
|
256
|
+
const warnings = [];
|
|
150
257
|
const arkrulesDir = path.join(canonicalRoot, 'arkrules');
|
|
151
258
|
const resolvedArkRulesDir = fs.existsSync(arkrulesDir)
|
|
152
259
|
? fs.realpathSync(arkrulesDir)
|
|
@@ -168,13 +275,105 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
|
|
|
168
275
|
}
|
|
169
276
|
}
|
|
170
277
|
}
|
|
278
|
+
return warnings;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ARKRULE_FILE_UNREFERENCED = 'ARKRULE_FILE_UNREFERENCED';
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Loader drift warnings as advisory check warnings (never fail --strict-config).
|
|
285
|
+
* @param {Array<{path:string,message:string}>} warnings
|
|
286
|
+
*/
|
|
287
|
+
export function arkRulesDriftWarnings(warnings) {
|
|
288
|
+
return (warnings ?? []).map((warning) => ({
|
|
289
|
+
ruleId: ARKRULE_FILE_UNREFERENCED,
|
|
290
|
+
file: warning.path,
|
|
291
|
+
line: 1,
|
|
292
|
+
message: warning.message,
|
|
293
|
+
severity: 'warning',
|
|
294
|
+
failsStrict: false,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {string} root
|
|
300
|
+
* @param {Record<string, unknown>} config loaded ark.config.json object
|
|
301
|
+
* @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
|
|
302
|
+
* @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, warnings: Array<{path:string,message:string,severity:string}>, errors: Array<{path:string,message:string}> }}
|
|
303
|
+
*/
|
|
304
|
+
function governedRelativeFiles(canonicalRoot, config) {
|
|
305
|
+
const include = Array.isArray(config?.include) && config.include.length > 0 ? config.include : ['src'];
|
|
306
|
+
return collectGovernedFiles(canonicalRoot, { ...config, include })
|
|
307
|
+
.map((file) => path.relative(canonicalRoot, file).split(path.sep).join('/'))
|
|
308
|
+
.filter((rel) => rel && !rel.startsWith('..'));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sliceRuleBasenames(config) {
|
|
312
|
+
return [...declaredLayerNames(config)].map((layer) => `arkrules.${layer}.json`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** `arkrules.<Layer>.json` beside a governed file or one of its parents. Not a repo-wide search. */
|
|
316
|
+
function unreferencedSliceRuleWarnings(canonicalRoot, referenced, files, basenames) {
|
|
317
|
+
const wanted = new Set(basenames);
|
|
318
|
+
if (wanted.size === 0) return [];
|
|
319
|
+
const dirs = new Set();
|
|
320
|
+
for (const rel of files) {
|
|
321
|
+
let end = String(rel).lastIndexOf('/');
|
|
322
|
+
while (end > 0) {
|
|
323
|
+
const dir = String(rel).slice(0, end);
|
|
324
|
+
if (dirs.has(dir)) break;
|
|
325
|
+
dirs.add(dir);
|
|
326
|
+
end = dir.lastIndexOf('/');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const warnings = [];
|
|
330
|
+
for (const dir of dirs) {
|
|
331
|
+
let names;
|
|
332
|
+
try {
|
|
333
|
+
names = fs.readdirSync(path.join(canonicalRoot, ...dir.split('/')));
|
|
334
|
+
} catch {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
for (const name of names) {
|
|
338
|
+
if (!wanted.has(name)) continue;
|
|
339
|
+
const candidate = `${dir}/${name}`;
|
|
340
|
+
if (referenced.has(candidate)) continue;
|
|
341
|
+
warnings.push({
|
|
342
|
+
path: candidate,
|
|
343
|
+
message: `ArkRules file ${JSON.stringify(candidate)} is not referenced by arkRules and will not be enforced`,
|
|
344
|
+
severity: 'advisory',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return warnings;
|
|
349
|
+
}
|
|
171
350
|
|
|
172
|
-
|
|
173
|
-
|
|
351
|
+
export function arkRulesLoadFailed(source, errors) {
|
|
352
|
+
const message = (errors ?? []).map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
|
|
353
|
+
const contractError = (errors ?? []).some(
|
|
354
|
+
(issue) =>
|
|
355
|
+
typeof issue.message === 'string' &&
|
|
356
|
+
(issue.message.includes('ARKRULE_DUPLICATE_ID') || issue.message.includes('ARKRULE_SCOPE_ESCAPES_SLICE'))
|
|
357
|
+
);
|
|
358
|
+
const label = contractError ? 'Invalid ArkGate config' : 'Invalid Effective Contract';
|
|
359
|
+
const error = new Error(`${label} (${source}):\n${message}`);
|
|
360
|
+
error.code = 'ARKRULES_LOAD_FAILED';
|
|
361
|
+
error.issues = errors;
|
|
362
|
+
if (contractError) {
|
|
363
|
+
error.name = 'ArkConfigValidationError';
|
|
364
|
+
error.source = source;
|
|
174
365
|
}
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
366
|
+
return error;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
|
|
370
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
371
|
+
const files = Array.isArray(opts.files) ? opts.files : governedRelativeFiles(canonicalRoot, config);
|
|
372
|
+
const loaded = loadEffectiveArkRules(config, directoryArkRulesReader(canonicalRoot, opts), { ...opts, files });
|
|
373
|
+
// Drift runs with or without a map: an arkrules/ directory nobody references is visible.
|
|
374
|
+
const warnings = [
|
|
375
|
+
...unreferencedArkRulesWarnings(canonicalRoot, loaded.referenced),
|
|
376
|
+
...unreferencedSliceRuleWarnings(canonicalRoot, loaded.referenced, files, sliceRuleBasenames(config)),
|
|
377
|
+
].sort((left, right) => left.path.localeCompare(right.path));
|
|
378
|
+
return { arkRules: loaded.arkRules, warnings, errors: loaded.errors };
|
|
180
379
|
}
|
|
@@ -10,6 +10,35 @@ import {
|
|
|
10
10
|
HOST_SUPPORT_MATRIX,
|
|
11
11
|
} from './host-support-matrix.mjs';
|
|
12
12
|
import { KNOWN_TOOLS, normalizeToolsList } from './skill-install.mjs';
|
|
13
|
+
import {
|
|
14
|
+
antigravityHooks,
|
|
15
|
+
claudeSettings,
|
|
16
|
+
codexHooks,
|
|
17
|
+
cursorHooks,
|
|
18
|
+
mergeAntigravityArkHook,
|
|
19
|
+
mergeClaudeStyleArkHooks,
|
|
20
|
+
mergeCursorArkHook,
|
|
21
|
+
} from './hook-templates.mjs';
|
|
22
|
+
|
|
23
|
+
/** Hosts whose hook file the installer merges (keeping user keys) instead of replacing. */
|
|
24
|
+
const MERGEABLE_HOOK_FILES = {
|
|
25
|
+
claude: [claudeSettings, mergeClaudeStyleArkHooks],
|
|
26
|
+
codex: [codexHooks, mergeClaudeStyleArkHooks],
|
|
27
|
+
cursor: [cursorHooks, mergeCursorArkHook],
|
|
28
|
+
antigravity: [antigravityHooks, mergeAntigravityArkHook],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** True when install will upsert Ark's hook into the existing host file without --force. */
|
|
32
|
+
export function hostHookFileMergeable(root, host, hookFile) {
|
|
33
|
+
const entry = MERGEABLE_HOOK_FILES[host];
|
|
34
|
+
if (!entry) return false;
|
|
35
|
+
try {
|
|
36
|
+
const [template, merge] = entry;
|
|
37
|
+
return merge(fs.readFileSync(hookFile, 'utf8'), template(root)) != null;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
13
42
|
|
|
14
43
|
export const HOST_ENFORCEMENT_SUPPORT = Object.freeze(
|
|
15
44
|
Object.fromEntries(
|
|
@@ -79,12 +108,19 @@ export function validateHardWriteRequest({ root, host, tools, force = false }) {
|
|
|
79
108
|
}
|
|
80
109
|
|
|
81
110
|
const hookFile = path.join(root, support.hookPath);
|
|
82
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
fs.existsSync(hookFile) &&
|
|
113
|
+
!force &&
|
|
114
|
+
!hasHardWriteHook(root, normalizedHost) &&
|
|
115
|
+
!hostHookFileMergeable(root, normalizedHost, hookFile)
|
|
116
|
+
) {
|
|
83
117
|
return {
|
|
84
118
|
ok: false,
|
|
85
|
-
error:
|
|
86
|
-
`${support.hookPath} already exists
|
|
87
|
-
|
|
119
|
+
error: MERGEABLE_HOOK_FILES[normalizedHost]
|
|
120
|
+
? `${support.hookPath} already exists but is not a JSON object Ark can merge into, so it would be preserved. ` +
|
|
121
|
+
'Fix that file (or use --force to replace it), or omit --require-write-hook for merge-only enforcement.'
|
|
122
|
+
: `${support.hookPath} already exists without an Ark hard-write hook and would be preserved. ` +
|
|
123
|
+
'Use --force to replace that host file, or omit --require-write-hook for merge-only enforcement.',
|
|
88
124
|
};
|
|
89
125
|
}
|
|
90
126
|
|