arkgate 4.8.22 → 4.8.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +27 -78
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +1117 -38
- package/bin/ark-mcp-runtime.mjs +412 -138
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +39 -0
- 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/agent-projection-command.mjs +5 -1
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +50 -6
- package/bin/lib/ark-order-doctor.mjs +1 -1
- package/bin/lib/ark-order-facts.mjs +410 -15
- package/bin/lib/ark-order-invariants.mjs +51 -9
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +2 -0
- package/bin/lib/ark-run-doctor.mjs +14 -0
- package/bin/lib/ark-run-facts.mjs +434 -11
- package/bin/lib/ark-run-sensors.mjs +5 -3
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrules-sensors.mjs +50 -249
- package/bin/lib/check-args.mjs +23 -1
- package/bin/lib/ci-and-commands.mjs +16 -6
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/codex-home.mjs +4 -2
- package/bin/lib/config-contract-slices.mjs +701 -0
- package/bin/lib/config-contract.mjs +5 -2
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +45 -6
- package/bin/lib/doctor-advisories.mjs +84 -1
- package/bin/lib/doctor-next-actions.mjs +17 -2
- package/bin/lib/doctor-plan.mjs +7 -9
- package/bin/lib/effective-contract-load.mjs +121 -59
- package/bin/lib/enforcement-honesty.mjs +22 -0
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/first-run-help.mjs +3 -2
- package/bin/lib/flat-parent-importers.mjs +137 -0
- package/bin/lib/flat-parent-pilot.mjs +192 -0
- package/bin/lib/gate-files.mjs +29 -2
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +1 -1
- package/bin/lib/hook-templates.mjs +18 -16
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +3 -1
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage.mjs +59 -11
- package/bin/lib/mcp-adoption.mjs +9 -1
- package/bin/lib/mcp-hook-payload.mjs +401 -61
- package/bin/lib/package-manager.mjs +59 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +81 -0
- package/bin/lib/physical-cohesion.mjs +16 -18
- package/bin/lib/pilot-loop.mjs +21 -8
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +168 -10
- package/bin/lib/prepare-change.mjs +120 -6
- package/bin/lib/prepare-write.mjs +1 -0
- package/bin/lib/presets.mjs +3 -5
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +315 -55
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +179 -5
- package/bin/lib/scan-files.mjs +32 -0
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/stable-hash.mjs +90 -0
- package/bin/lib/start-install-recovery.mjs +71 -1
- package/bin/lib/start-preview.mjs +30 -0
- package/bin/lib/status-command.mjs +33 -4
- package/bin/lib/status-manifest.mjs +32 -4
- package/bin/lib/team-parliament-io.mjs +79 -75
- package/bin/lib/team-parliament.mjs +6 -1
- package/bin/lib/write-path-capabilities.mjs +4 -2
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-MBXCFLO4.js +16 -0
- package/dist/chunk-RNQHRDCQ.cjs +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/{configTypes-CTELo87-.d.ts → configTypes-BJbaeoj4.d.cts} +53 -4
- package/dist/configTypes-BJbaeoj4.d.ts +246 -0
- package/dist/eslint/index.cjs +17 -4
- package/dist/eslint/index.d.cts +359 -0
- package/dist/eslint/index.d.ts +178 -158
- package/dist/eslint/index.js +10 -4
- package/dist/index.cjs +38 -37
- package/dist/index.d.cts +5475 -0
- package/dist/index.d.ts +3408 -296
- package/dist/index.js +38 -37
- 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-WD2TPSwZ.d.ts → runtime/index.d.cts} +814 -290
- package/dist/runtime/index.d.ts +2678 -108
- package/dist/runtime/index.js +13 -30
- package/dist/{types-yDNEQ603.d.ts → types-DRinG-vT.d.cts} +576 -140
- package/dist/types-DRinG-vT.d.ts +1792 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +92 -15
- package/docs/ai-gates.md +160 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +273 -12
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +122 -13
- package/docs/package-surface.md +31 -8
- package/docs/product-voice.md +1 -1
- package/package.json +59 -18
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +163 -4
- 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 +4 -1
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- 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 +4 -1
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-runtime.md +6 -1
- package/dist/types-fQ9-KrBj.d.ts +0 -183
|
@@ -1,6 +1,10 @@
|
|
|
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';
|
|
@@ -10,7 +14,7 @@ import {
|
|
|
10
14
|
loadArkRulesContract,
|
|
11
15
|
} from './arkrules-contract.mjs';
|
|
12
16
|
|
|
13
|
-
function normalizeProjectRelativePath(value) {
|
|
17
|
+
export function normalizeProjectRelativePath(value) {
|
|
14
18
|
const normalized = value.replace(/\\/g, '/');
|
|
15
19
|
if (
|
|
16
20
|
!normalized ||
|
|
@@ -29,7 +33,7 @@ function normalizeProjectRelativePath(value) {
|
|
|
29
33
|
return segments.length > 0 ? segments.join('/') : undefined;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
function isWithinRoot(root, candidate) {
|
|
36
|
+
export function isWithinRoot(root, candidate) {
|
|
33
37
|
const relative = path.relative(root, candidate);
|
|
34
38
|
return (
|
|
35
39
|
relative === '' ||
|
|
@@ -40,26 +44,29 @@ function isWithinRoot(root, candidate) {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
|
-
* @
|
|
47
|
+
* @typedef {{ ok: true, content: string } | { ok: false, missing?: boolean, message: string }} ArkRulesRead
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Core loader shared by every source.
|
|
52
|
+
*
|
|
44
53
|
* @param {Record<string, unknown>} config loaded ark.config.json object
|
|
45
|
-
* @param {
|
|
46
|
-
* @
|
|
54
|
+
* @param {(rel: string) => ArkRulesRead} readRelative
|
|
55
|
+
* @param {{ missingAsEmpty?: boolean }} [opts] missingAsEmpty: a referenced file absent
|
|
56
|
+
* from the source counts as an empty layer (a base that predates the file), not an error.
|
|
57
|
+
* @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, errors: Array<{path:string,message:string}>, referenced: Set<string> }}
|
|
47
58
|
*/
|
|
48
|
-
export function
|
|
59
|
+
export function loadEffectiveArkRules(config, readRelative, opts = {}) {
|
|
49
60
|
const refs = config?.arkRules;
|
|
61
|
+
const referenced = new Set();
|
|
50
62
|
if (!refs || typeof refs !== 'object' || Object.keys(refs).length === 0) {
|
|
51
|
-
return { arkRules: emptyEffectiveArkRules(),
|
|
63
|
+
return { arkRules: emptyEffectiveArkRules(), errors: [], referenced };
|
|
52
64
|
}
|
|
53
|
-
|
|
54
65
|
const layerNames = new Set(
|
|
55
66
|
Array.isArray(config.layers) ? config.layers.map((layer) => layer.name) : []
|
|
56
67
|
);
|
|
57
68
|
const errors = [];
|
|
58
|
-
const warnings = [];
|
|
59
69
|
const parts = [];
|
|
60
|
-
const referenced = new Set();
|
|
61
|
-
const canonicalRoot = fs.realpathSync(root);
|
|
62
|
-
|
|
63
70
|
for (const layer of Object.keys(refs).sort()) {
|
|
64
71
|
const relRaw = refs[layer];
|
|
65
72
|
const pathKey = `$.arkRules[${JSON.stringify(layer)}]`;
|
|
@@ -83,70 +90,102 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
|
|
|
83
90
|
});
|
|
84
91
|
continue;
|
|
85
92
|
}
|
|
86
|
-
|
|
87
93
|
referenced.add(rel);
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
94
|
+
const read = readRelative(rel);
|
|
95
|
+
if (!read.ok) {
|
|
96
|
+
if (read.missing && opts.missingAsEmpty) continue;
|
|
97
|
+
errors.push({ path: pathKey, message: read.message });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const loaded = loadArkRulesContract(JSON.parse(read.content), rel, layer);
|
|
102
|
+
parts.push({ layer, sourceFile: rel, file: loaded.config });
|
|
103
|
+
} catch (error) {
|
|
90
104
|
errors.push({
|
|
91
105
|
path: pathKey,
|
|
92
|
-
message:
|
|
106
|
+
message:
|
|
107
|
+
error instanceof Error
|
|
108
|
+
? error.message
|
|
109
|
+
: `referenced ArkRules file ${JSON.stringify(rel)} failed to load`,
|
|
93
110
|
});
|
|
94
|
-
continue;
|
|
95
111
|
}
|
|
112
|
+
}
|
|
113
|
+
if (errors.length > 0) return { arkRules: emptyEffectiveArkRules(), errors, referenced };
|
|
114
|
+
return { arkRules: buildEffectiveArkRules(parts), errors: [], referenced };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Directory reader: realpath + within-root guards, then readFileSync.
|
|
119
|
+
* @param {string} canonicalRoot realpath of the directory the paths are relative to
|
|
120
|
+
* @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
|
|
121
|
+
* @returns {(rel: string) => ArkRulesRead}
|
|
122
|
+
*/
|
|
123
|
+
export function directoryArkRulesReader(canonicalRoot, opts = {}) {
|
|
124
|
+
return (rel) => {
|
|
125
|
+
const lexicalTarget = path.resolve(canonicalRoot, ...rel.split('/'));
|
|
126
|
+
const outside = {
|
|
127
|
+
ok: false,
|
|
128
|
+
message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
|
|
129
|
+
};
|
|
130
|
+
if (!isWithinRoot(canonicalRoot, lexicalTarget)) return outside;
|
|
96
131
|
if (!fs.existsSync(lexicalTarget)) {
|
|
97
|
-
|
|
98
|
-
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
missing: true,
|
|
99
135
|
message: `referenced ArkRules file ${JSON.stringify(rel)} is missing`,
|
|
100
|
-
}
|
|
101
|
-
continue;
|
|
136
|
+
};
|
|
102
137
|
}
|
|
103
138
|
let absolute;
|
|
104
139
|
try {
|
|
105
140
|
absolute = fs.realpathSync(lexicalTarget);
|
|
106
141
|
} catch (error) {
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
109
144
|
message: `referenced ArkRules file ${JSON.stringify(rel)} could not be resolved: ${
|
|
110
145
|
error instanceof Error ? error.message : String(error)
|
|
111
146
|
}`,
|
|
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;
|
|
147
|
+
};
|
|
121
148
|
}
|
|
149
|
+
if (!isWithinRoot(canonicalRoot, absolute)) return outside;
|
|
122
150
|
opts.observeInput?.(absolute, 'arkrules');
|
|
123
|
-
let content;
|
|
124
151
|
try {
|
|
125
|
-
content
|
|
152
|
+
return { ok: true, content: fs.readFileSync(absolute, 'utf8') };
|
|
126
153
|
} catch (error) {
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
129
156
|
message: `referenced ArkRules file ${JSON.stringify(rel)} could not be read: ${
|
|
130
157
|
error instanceof Error ? error.message : String(error)
|
|
131
158
|
}`,
|
|
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
|
-
});
|
|
159
|
+
};
|
|
146
160
|
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Reader over supplied ArkRules file objects keyed by project-relative path
|
|
166
|
+
* (MCP callers hand a catalog as data instead of a checkout).
|
|
167
|
+
* @param {Record<string, unknown>} files
|
|
168
|
+
* @returns {(rel: string) => ArkRulesRead}
|
|
169
|
+
*/
|
|
170
|
+
export function objectArkRulesReader(files) {
|
|
171
|
+
const byPath = new Map();
|
|
172
|
+
for (const [key, value] of Object.entries(files ?? {})) {
|
|
173
|
+
const rel = normalizeProjectRelativePath(String(key));
|
|
174
|
+
if (rel) byPath.set(rel, value);
|
|
147
175
|
}
|
|
176
|
+
return (rel) =>
|
|
177
|
+
byPath.has(rel)
|
|
178
|
+
? { ok: true, content: JSON.stringify(byPath.get(rel)) }
|
|
179
|
+
: {
|
|
180
|
+
ok: false,
|
|
181
|
+
missing: true,
|
|
182
|
+
message: `ArkRules file ${JSON.stringify(rel)} was not supplied`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
148
185
|
|
|
149
|
-
|
|
186
|
+
/** Advisory drift: `arkrules/*.json` files the arkRules map does not reference (ADR 0012 D2). */
|
|
187
|
+
function unreferencedArkRulesWarnings(canonicalRoot, referenced) {
|
|
188
|
+
const warnings = [];
|
|
150
189
|
const arkrulesDir = path.join(canonicalRoot, 'arkrules');
|
|
151
190
|
const resolvedArkRulesDir = fs.existsSync(arkrulesDir)
|
|
152
191
|
? fs.realpathSync(arkrulesDir)
|
|
@@ -168,13 +207,36 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
|
|
|
168
207
|
}
|
|
169
208
|
}
|
|
170
209
|
}
|
|
210
|
+
return warnings;
|
|
211
|
+
}
|
|
171
212
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
export const ARKRULE_FILE_UNREFERENCED = 'ARKRULE_FILE_UNREFERENCED';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Loader drift warnings as advisory check warnings (never fail --strict-config).
|
|
217
|
+
* @param {Array<{path:string,message:string}>} warnings
|
|
218
|
+
*/
|
|
219
|
+
export function arkRulesDriftWarnings(warnings) {
|
|
220
|
+
return (warnings ?? []).map((warning) => ({
|
|
221
|
+
ruleId: ARKRULE_FILE_UNREFERENCED,
|
|
222
|
+
file: warning.path,
|
|
223
|
+
line: 1,
|
|
224
|
+
message: warning.message,
|
|
225
|
+
severity: 'warning',
|
|
226
|
+
failsStrict: false,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {string} root
|
|
232
|
+
* @param {Record<string, unknown>} config loaded ark.config.json object
|
|
233
|
+
* @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
|
|
234
|
+
* @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, warnings: Array<{path:string,message:string,severity:string}>, errors: Array<{path:string,message:string}> }}
|
|
235
|
+
*/
|
|
236
|
+
export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
|
|
237
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
238
|
+
const loaded = loadEffectiveArkRules(config, directoryArkRulesReader(canonicalRoot, opts));
|
|
239
|
+
// Drift runs with or without a map: an arkrules/ directory nobody references is visible.
|
|
240
|
+
const warnings = unreferencedArkRulesWarnings(canonicalRoot, loaded.referenced);
|
|
241
|
+
return { arkRules: loaded.arkRules, warnings, errors: loaded.errors };
|
|
180
242
|
}
|
|
@@ -289,6 +289,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
|
|
|
289
289
|
* operatingMode?: string | null,
|
|
290
290
|
* activeBlockingViolations?: number | null,
|
|
291
291
|
* physicalCohesionResidual?: boolean,
|
|
292
|
+
* childWallAdvisory?: boolean,
|
|
293
|
+
* sliceAliasDebt?: boolean,
|
|
292
294
|
* }} input
|
|
293
295
|
*/
|
|
294
296
|
export function physicalCohesionResidualRemains(cohesion) {
|
|
@@ -489,6 +491,22 @@ export function buildProductHonesty(input = {}) {
|
|
|
489
491
|
// Informational only when no enforced arkrule plane — does not alone make unfinished.
|
|
490
492
|
}
|
|
491
493
|
|
|
494
|
+
if (input.childWallAdvisory === true) {
|
|
495
|
+
reasons.push({
|
|
496
|
+
id: 'child-wall-advisory',
|
|
497
|
+
message:
|
|
498
|
+
'A child wall lists siblings as advisory. Recorded crossings stay non-blocking, and a new sibling crossing past that baseline still fails. An enforce list makes those subtrees errors and does not finish the house. This house stays unfinished until siblings is deny.',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (input.sliceAliasDebt === true) {
|
|
503
|
+
reasons.push({
|
|
504
|
+
id: 'slice-alias-debt',
|
|
505
|
+
message:
|
|
506
|
+
'Slice aliases are an owed move, not a destination. Aliased files are not finished until they move into the target slice folder.',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
492
510
|
// EH05: environment residual deny-list (future reason ids stay architecture debt by default).
|
|
493
511
|
const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host', 'native-fail-open']);
|
|
494
512
|
|
|
@@ -683,6 +701,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
683
701
|
nativeFailClosed,
|
|
684
702
|
nativeFailClosedPolicy,
|
|
685
703
|
physicalCohesionResidual,
|
|
704
|
+
childWallAdvisory,
|
|
705
|
+
sliceAliasDebt,
|
|
686
706
|
} = {}) {
|
|
687
707
|
const coverageHonesty = buildCoverageHonesty({
|
|
688
708
|
percent: governedPercent,
|
|
@@ -741,6 +761,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
741
761
|
emptyStewards,
|
|
742
762
|
stewardNudge,
|
|
743
763
|
physicalCohesionResidual,
|
|
764
|
+
childWallAdvisory,
|
|
765
|
+
sliceAliasDebt,
|
|
744
766
|
});
|
|
745
767
|
return {
|
|
746
768
|
coverageHonesty,
|
|
@@ -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
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { NORTH_STAR_ONE_LINE } from './product-copy.mjs';
|
|
6
|
+
import { KNOWN_TOOLS } from './skill-install.mjs';
|
|
6
7
|
|
|
7
8
|
export function setupUsage() {
|
|
8
9
|
return `arkgate (alias ark) — Write. Check. Ship.
|
|
@@ -96,8 +97,8 @@ Options:
|
|
|
96
97
|
Valid ids: crud-product, api-backend, frontend-surface, library-sdk, cli-utility,
|
|
97
98
|
worker-pipeline, event-coordinator, integration-bridge, multi-app-workspace, prototype-spike,
|
|
98
99
|
vertical-slice-product, ddd-bounded-contexts.
|
|
99
|
-
--tools One active agent host for start (
|
|
100
|
-
Omit to use the active host; an unknown host
|
|
100
|
+
--tools One active agent host for start (${KNOWN_TOOLS.join(',')}).
|
|
101
|
+
Omit to use the active host; a list or an unknown host is rejected (exit 2).
|
|
101
102
|
--remove-host <host>
|
|
102
103
|
Preview or apply removal of that compact host integration; re-add it with --tools <host>.
|
|
103
104
|
--require-write-hook <host>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importer graph for the flat-parent doctor suggestion.
|
|
3
|
+
*
|
|
4
|
+
* Prefers the resolved dependency facts ark-check already built (tsconfig
|
|
5
|
+
* paths included). Without facts it scans the text and resolves relative
|
|
6
|
+
* specifiers plus tsconfig path aliases, so an `@/…` importer is never
|
|
7
|
+
* invisible. Advisory input only.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { readTsconfigAliases, resolveSpecifierToRel } from './import-resolve.mjs';
|
|
12
|
+
|
|
13
|
+
const SPEC_MARKS = [' from "', " from '", ' from `', 'require("', "require('", 'import("', "import('"];
|
|
14
|
+
const EXT = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
15
|
+
|
|
16
|
+
function withoutComments(text) {
|
|
17
|
+
let out = '';
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < text.length) {
|
|
20
|
+
const c = text[i];
|
|
21
|
+
const n = text[i + 1];
|
|
22
|
+
if (c === '/' && n === '/') {
|
|
23
|
+
while (i < text.length && text[i] !== '\n') i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (c === '/' && n === '*') {
|
|
27
|
+
i += 2;
|
|
28
|
+
while (i + 1 < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
29
|
+
i = Math.min(text.length, i + 2);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
out += c;
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Every string module specifier in `text` (relative and bare). */
|
|
39
|
+
export function moduleSpecifiers(text) {
|
|
40
|
+
const specs = [];
|
|
41
|
+
const source = withoutComments(String(text));
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < source.length) {
|
|
44
|
+
let at = -1;
|
|
45
|
+
let mark = '';
|
|
46
|
+
for (const candidate of SPEC_MARKS) {
|
|
47
|
+
const found = source.indexOf(candidate, i);
|
|
48
|
+
if (found !== -1 && (at === -1 || found < at)) {
|
|
49
|
+
at = found;
|
|
50
|
+
mark = candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (at === -1) break;
|
|
54
|
+
const quote = mark[mark.length - 1];
|
|
55
|
+
const start = at + mark.length;
|
|
56
|
+
const end = source.indexOf(quote, start);
|
|
57
|
+
if (end === -1) break;
|
|
58
|
+
const spec = source.slice(start, end);
|
|
59
|
+
if (spec.length > 0) specs.push(spec);
|
|
60
|
+
i = end + 1;
|
|
61
|
+
}
|
|
62
|
+
return specs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveRelative(fromFile, spec) {
|
|
66
|
+
const base = fromFile.split('/');
|
|
67
|
+
base.pop();
|
|
68
|
+
for (const part of spec.split('/')) {
|
|
69
|
+
if (part === '' || part === '.') continue;
|
|
70
|
+
if (part === '..') {
|
|
71
|
+
if (base.length === 0) return null;
|
|
72
|
+
base.pop();
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
base.push(part);
|
|
76
|
+
}
|
|
77
|
+
return base.join('/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function probe(joined, fileSet) {
|
|
81
|
+
if (!joined) return null;
|
|
82
|
+
const names = [joined];
|
|
83
|
+
for (const ext of EXT) names.push(`${joined}${ext}`, `${joined}/index${ext}`);
|
|
84
|
+
const stem = joined.replace(/\.(?:m|c)?js$/, '');
|
|
85
|
+
if (stem !== joined) for (const ext of EXT) names.push(`${stem}${ext}`);
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
if (fileSet.has(name)) return name;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readText(root, rel) {
|
|
93
|
+
try {
|
|
94
|
+
return fs.readFileSync(path.join(root, rel), 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function addEdge(graph, target, importer) {
|
|
101
|
+
let set = graph.get(target);
|
|
102
|
+
if (!set) {
|
|
103
|
+
set = new Set();
|
|
104
|
+
graph.set(target, set);
|
|
105
|
+
}
|
|
106
|
+
set.add(importer);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* target file → every file that imports it (value and type-only edges; a
|
|
111
|
+
* peerIsolation wall judges both).
|
|
112
|
+
* @param {{ root: string, files: string[], facts?: object, ts?: object }} input
|
|
113
|
+
*/
|
|
114
|
+
export function flatParentImporterGraph({ root, files, facts, ts }) {
|
|
115
|
+
const graph = new Map();
|
|
116
|
+
const fileSet = new Set(files);
|
|
117
|
+
const dependencies = Array.isArray(facts?.dependencies) ? facts.dependencies : null;
|
|
118
|
+
if (dependencies && dependencies.length > 0) {
|
|
119
|
+
for (const dep of dependencies) {
|
|
120
|
+
if (dep?.resolution !== 'resolved-project' || typeof dep.target !== 'string') continue;
|
|
121
|
+
if (typeof dep.from !== 'string' || dep.from === dep.target) continue;
|
|
122
|
+
addEdge(graph, dep.target, dep.from);
|
|
123
|
+
}
|
|
124
|
+
return graph;
|
|
125
|
+
}
|
|
126
|
+
const aliases = readTsconfigAliases(ts, root);
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
for (const spec of moduleSpecifiers(readText(root, file))) {
|
|
129
|
+
const joined = spec.startsWith('.')
|
|
130
|
+
? resolveRelative(file, spec)
|
|
131
|
+
: resolveSpecifierToRel(spec, file, root, aliases);
|
|
132
|
+
const target = probe(joined, fileSet);
|
|
133
|
+
if (target && target !== file) addEdge(graph, target, file);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return graph;
|
|
137
|
+
}
|