arkgate 2.11.0 → 2.13.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 +147 -0
- package/README.md +70 -41
- package/bin/ark-check.mjs +95 -36
- package/bin/ark-mcp.mjs +11 -5
- package/bin/ark-shared.mjs +88 -56
- package/bin/ark.mjs +97 -29
- package/bin/lib/agent-gates.mjs +79 -2093
- package/bin/lib/architecture-scan.mjs +8 -0
- package/bin/lib/ci-and-commands.mjs +392 -0
- package/bin/lib/codex-home.mjs +7 -0
- package/bin/lib/config-contract.mjs +331 -0
- package/bin/lib/deploy-path.mjs +205 -0
- package/bin/lib/doctor-plan.mjs +43 -16
- package/bin/lib/enforcement-profiles.mjs +97 -0
- package/bin/lib/gate-files.mjs +223 -0
- package/bin/lib/hook-templates.mjs +99 -0
- package/bin/lib/host-support-matrix.mjs +77 -0
- package/bin/lib/install-migrate.mjs +473 -0
- package/bin/lib/mcp-adoption.mjs +455 -0
- package/bin/lib/open-html.mjs +75 -0
- package/bin/lib/presets.mjs +6 -2
- package/bin/lib/safety-diagnostics.mjs +31 -11
- package/bin/lib/skill-install.mjs +323 -0
- package/bin/lib/ts-resolve.mjs +2 -1
- package/bin/lib/typescript-host.mjs +88 -0
- package/bin/lib/weakest-link.mjs +417 -0
- package/bin/lib/write-path-capabilities.mjs +182 -0
- package/bin/lib/write-path-detect.mjs +101 -0
- package/dist/configContract-iBLxx5Tz.d.cts +53 -0
- package/dist/configContract-iBLxx5Tz.d.ts +53 -0
- package/dist/eslint/index.cjs +375 -13
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.cts +30 -20
- package/dist/eslint/index.d.ts +30 -20
- package/dist/eslint/index.js +375 -13
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +723 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -5
- package/dist/index.d.ts +95 -5
- package/dist/index.js +716 -61
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +150 -42
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +2 -1
- package/dist/nestjs/index.d.ts +2 -1
- package/dist/nestjs/index.js +150 -42
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +723 -61
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +3 -2
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +716 -61
- package/dist/runtime/index.js.map +1 -1
- package/dist/{types-BZ17b9i5.d.cts → types-BxBwnBpC.d.cts} +9 -36
- package/dist/{types-BZ17b9i5.d.ts → types-Wcs_l1_J.d.ts} +9 -36
- package/docs/agent-guide.md +43 -21
- package/docs/ai-gates.md +53 -18
- package/docs/configuration.md +97 -0
- package/docs/enthusiast/README.md +3 -3
- package/docs/enthusiast/how-to-agent-gates.md +7 -3
- package/docs/migrate-from-ark-runtime-kernel.md +3 -0
- package/docs/package-surface.md +22 -10
- package/docs/production-hardening.md +15 -2
- package/docs/threat-model.md +65 -0
- package/docs/typescript-support.md +3 -3
- package/package.json +15 -2
- package/schemas/ark.config.schema.json +750 -0
- package/server.json +2 -2
- package/templates/hooks/pre-commit-ark +37 -0
- package/templates/skills/ark-autopilot.md +77 -45
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-explain.md +2 -1
- package/templates/skills/ark-explore.md +135 -34
- package/templates/skills/ark-runtime.md +8 -5
- package/templates/skills/ark-upgrade.md +36 -16
- package/tests/fixtures/ts-consumer/ark.config.json +2 -0
package/bin/ark-check.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
loadTypeScript,
|
|
34
34
|
detectSkillGaps,
|
|
35
35
|
detectCodexHomeGap,
|
|
36
|
+
detectActiveAgentHost,
|
|
36
37
|
missingGates,
|
|
37
38
|
staleRunnerGateFiles,
|
|
38
39
|
brokenMcpGateFiles,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
reportsDir,
|
|
57
58
|
readJsonSafe,
|
|
58
59
|
} from './lib/html-report.mjs';
|
|
60
|
+
import { shouldOpenHtmlReport, openHtmlInBrowser } from './lib/open-html.mjs';
|
|
59
61
|
import {
|
|
60
62
|
computeCoverage,
|
|
61
63
|
runCoverage,
|
|
@@ -81,6 +83,7 @@ import {
|
|
|
81
83
|
import {
|
|
82
84
|
ARCHITECTURE_PRESETS,
|
|
83
85
|
} from './lib/presets.mjs';
|
|
86
|
+
import { loadArkConfigContract, parseArkConfigJson } from './lib/config-contract.mjs';
|
|
84
87
|
|
|
85
88
|
import {
|
|
86
89
|
collectGovernedFiles,
|
|
@@ -91,6 +94,7 @@ import {
|
|
|
91
94
|
configWarning,
|
|
92
95
|
} from './lib/config-warnings.mjs';
|
|
93
96
|
import { runArchitectureScan } from './lib/architecture-scan.mjs';
|
|
97
|
+
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
94
98
|
|
|
95
99
|
|
|
96
100
|
function parseArgs(argv) {
|
|
@@ -101,9 +105,9 @@ function parseArgs(argv) {
|
|
|
101
105
|
printConfig: undefined,
|
|
102
106
|
tsconfig: undefined,
|
|
103
107
|
json: false,
|
|
104
|
-
strict: false,
|
|
105
108
|
strictConfig: false,
|
|
106
109
|
requireGates: false,
|
|
110
|
+
requireWriteHook: undefined,
|
|
107
111
|
init: false,
|
|
108
112
|
installAgentGates: false,
|
|
109
113
|
tools: undefined,
|
|
@@ -122,6 +126,8 @@ function parseArgs(argv) {
|
|
|
122
126
|
applyPolicyPack: undefined,
|
|
123
127
|
watch: false,
|
|
124
128
|
beginner: false,
|
|
129
|
+
openReport: false,
|
|
130
|
+
noOpenReport: false,
|
|
125
131
|
version: false,
|
|
126
132
|
help: false,
|
|
127
133
|
};
|
|
@@ -135,13 +141,15 @@ function parseArgs(argv) {
|
|
|
135
141
|
for (let i = 2; i < argv.length; i += 1) {
|
|
136
142
|
const arg = argv[i];
|
|
137
143
|
if (arg === '--json') args.json = true;
|
|
138
|
-
else if (arg === '--strict') {
|
|
139
|
-
args.strict = true;
|
|
144
|
+
else if (arg === '--strict' || arg === '--strict-merge') {
|
|
140
145
|
args.strictConfig = true;
|
|
141
146
|
args.requireGates = true;
|
|
142
147
|
}
|
|
143
148
|
else if (arg === '--strict-config') args.strictConfig = true;
|
|
144
149
|
else if (arg === '--require-gates') args.requireGates = true;
|
|
150
|
+
else if (arg === '--require-write-hook') {
|
|
151
|
+
args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
|
|
152
|
+
}
|
|
145
153
|
else if (arg === '--init') args.init = true;
|
|
146
154
|
else if (arg === '--preset') args.preset = requireValue(arg, i++);
|
|
147
155
|
else if (arg === '--install-agent-gates') args.installAgentGates = true;
|
|
@@ -183,6 +191,8 @@ function parseArgs(argv) {
|
|
|
183
191
|
}
|
|
184
192
|
else if (arg === '--reset-origin') args.resetOrigin = true;
|
|
185
193
|
else if (arg === '--no-archive') args.noArchive = true;
|
|
194
|
+
else if (arg === '--open') args.openReport = true;
|
|
195
|
+
else if (arg === '--no-open') args.noOpenReport = true;
|
|
186
196
|
else if (arg === '--baseline' || arg === '--update-baseline') {
|
|
187
197
|
if (arg === '--update-baseline') args.updateBaseline = true;
|
|
188
198
|
// optional path value: consume the next arg only when it isn't another flag
|
|
@@ -214,7 +224,7 @@ function usage() {
|
|
|
214
224
|
return [
|
|
215
225
|
'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
|
|
216
226
|
' ark-check --version',
|
|
217
|
-
' ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict | --strict-config] [--require-gates] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
227
|
+
' ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
218
228
|
' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
|
|
219
229
|
' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
|
|
220
230
|
' ark-check --recommend [--json] [--write-plan] application-shape plan; --write-plan emits ark-adoption-plan.json',
|
|
@@ -224,10 +234,11 @@ function usage() {
|
|
|
224
234
|
' ark-check --adopt-contract [--write] expand include + UI patterns from ungoverned dirs (contract adopt)',
|
|
225
235
|
' ark-check --ratchet-cores when raw graph is green (0 violations; baseline ignored), set optional:false on populated cores only (writes ark.config.json)',
|
|
226
236
|
' ark-check --watch re-run the check when governed files change (debounced)',
|
|
227
|
-
' ark-check --report [file.html] [--beginner] [--reset-origin] [--no-archive]',
|
|
237
|
+
' ark-check --report [file.html] [--beginner] [--reset-origin] [--no-archive] [--open|--no-open]',
|
|
228
238
|
' HTML report + snapshots under .ark/reports/ (origin once, latest each run, history JSON)',
|
|
239
|
+
' Best-effort open in browser (local TTY). No-op if open fails. --no-open / ARK_NO_OPEN_REPORT=1 to skip; --open forces open.',
|
|
229
240
|
' ark-check --init [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|clean-architecture|onion-architecture] [--force]',
|
|
230
|
-
' ark-check --install-agent-gates [--tools claude,cursor,codex,grok] [--skills-only] [--codex-home] [--force]',
|
|
241
|
+
' ark-check --install-agent-gates [--tools claude,cursor,codex,grok] [--require-write-hook <host>] [--skills-only] [--codex-home] [--force]',
|
|
231
242
|
' ark-check --update-baseline [file] freeze current violations (default .ark-baseline.json)',
|
|
232
243
|
' ark-check --print-config eleven-layer',
|
|
233
244
|
'',
|
|
@@ -267,8 +278,12 @@ function usage() {
|
|
|
267
278
|
'',
|
|
268
279
|
'Config warnings are advisory by default and are included in JSON output.',
|
|
269
280
|
'Use --strict-config to make config warnings fail the check.',
|
|
270
|
-
'Use --strict for the fail-closed CI profile: --strict-config + --require-gates',
|
|
271
|
-
'plus the security diagnostics surfaced by doctor.',
|
|
281
|
+
'Use --strict-merge for the fail-closed CI profile: --strict-config + --require-gates',
|
|
282
|
+
'plus the security diagnostics surfaced by doctor. --strict is a compatibility alias.',
|
|
283
|
+
'This merge profile never depends on an editor/agent hook.',
|
|
284
|
+
'Add --require-write-hook claude|grok to validate a hard local write boundary for that',
|
|
285
|
+
'specific host. Cursor and Codex expose advisory MCP tools plus the shared CI check;',
|
|
286
|
+
'merge blocking requires repository policy to make that status required.',
|
|
272
287
|
'',
|
|
273
288
|
'--require-gates fails the check when AGENTS.md, .mcp.json, or the generated CI',
|
|
274
289
|
'workflow is missing, so "installed but never configured" is a red CI. Combine it',
|
|
@@ -276,7 +291,8 @@ function usage() {
|
|
|
276
291
|
'',
|
|
277
292
|
'--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
|
|
278
293
|
'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok',
|
|
279
|
-
'(
|
|
294
|
+
'(Claude/Grok hard-write hooks; Cursor/Codex advisory MCP; shared CI check for all) and',
|
|
295
|
+
'windsurf, cline, copilot, kiro, roo, continue, gemini',
|
|
280
296
|
'(instruction-tier rule files derived from the same contract).',
|
|
281
297
|
'It also installs the /ark-* skills shipped in templates/skills/ into each',
|
|
282
298
|
'detected tool\'s command location (.claude/skills/, .cursor/commands/,',
|
|
@@ -290,7 +306,7 @@ function usage() {
|
|
|
290
306
|
'Pass --tools to pick which tool configs to write; otherwise they are auto-detected',
|
|
291
307
|
'from their config directories (.claude/, .cursor/, .codex/, .grok/, .windsurf/,',
|
|
292
308
|
'.clinerules/, .kiro/, .roo/, .continue/, .gemini/; copilot is explicit-only).',
|
|
293
|
-
'claude+cursor+codex are written when nothing is detected.',
|
|
309
|
+
'claude+cursor+codex+grok are written when nothing is detected.',
|
|
294
310
|
'',
|
|
295
311
|
'Generate a starter 11-layer config:',
|
|
296
312
|
' ark-check --print-config eleven-layer > ark.config.json',
|
|
@@ -305,25 +321,12 @@ function readConfig(root, configPath) {
|
|
|
305
321
|
? configPath
|
|
306
322
|
: path.join(root, configPath);
|
|
307
323
|
if (!fs.existsSync(fullPath)) {
|
|
308
|
-
return
|
|
309
|
-
include: ['src'],
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
};
|
|
324
|
+
return loadArkConfigContract(
|
|
325
|
+
{ include: ['src'], layers: [], rules: DEFAULT_RULES },
|
|
326
|
+
fullPath
|
|
327
|
+
).config;
|
|
313
328
|
}
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
include: raw.include ?? ['src'],
|
|
317
|
-
layers: raw.layers ?? [],
|
|
318
|
-
rules: raw.rules ?? DEFAULT_RULES,
|
|
319
|
-
...(raw.exclude ? { exclude: raw.exclude } : {}),
|
|
320
|
-
...(raw.excludeGenerated !== undefined ? { excludeGenerated: raw.excludeGenerated } : {}),
|
|
321
|
-
...(raw.cyclePolicy ? { cyclePolicy: raw.cyclePolicy } : {}),
|
|
322
|
-
...(raw.dynamicImportAllowlist
|
|
323
|
-
? { dynamicImportAllowlist: raw.dynamicImportAllowlist }
|
|
324
|
-
: {}),
|
|
325
|
-
...(raw.safety ? { safety: raw.safety } : {}),
|
|
326
|
-
};
|
|
329
|
+
return parseArkConfigJson(fs.readFileSync(fullPath, 'utf8'), fullPath).config;
|
|
327
330
|
}
|
|
328
331
|
|
|
329
332
|
/**
|
|
@@ -976,16 +979,41 @@ async function main() {
|
|
|
976
979
|
return;
|
|
977
980
|
}
|
|
978
981
|
|
|
979
|
-
if (args.requireGates) {
|
|
980
|
-
|
|
981
|
-
if (args.
|
|
982
|
-
|
|
982
|
+
if (args.requireGates || args.requireWriteHook) {
|
|
983
|
+
let writeRequest = null;
|
|
984
|
+
if (args.requireWriteHook) {
|
|
985
|
+
writeRequest = validateHardWriteRequest({
|
|
986
|
+
root: args.root,
|
|
987
|
+
host: args.requireWriteHook,
|
|
988
|
+
tools: [args.requireWriteHook],
|
|
989
|
+
force: true,
|
|
990
|
+
});
|
|
991
|
+
if (!writeRequest.ok) {
|
|
992
|
+
const payload = {
|
|
993
|
+
ok: false,
|
|
994
|
+
error: 'unsupported-enforcement-profile',
|
|
995
|
+
message: writeRequest.error,
|
|
996
|
+
};
|
|
997
|
+
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
998
|
+
else console.error(writeRequest.error);
|
|
999
|
+
process.exitCode = 2;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const missing = args.requireGates ? missingGates(args.root) : [];
|
|
1005
|
+
if (
|
|
1006
|
+
writeRequest?.host &&
|
|
1007
|
+
!detectWritePathCapabilities(args.root, writeRequest.host).capabilities['hard-write']
|
|
1008
|
+
) {
|
|
1009
|
+
missing.push(`${writeRequest.host} hard-write hook`);
|
|
983
1010
|
}
|
|
984
1011
|
if (missing.length > 0) {
|
|
985
1012
|
const payload = {
|
|
986
1013
|
ok: false,
|
|
987
1014
|
error: 'missing-gates',
|
|
988
1015
|
missing,
|
|
1016
|
+
...(writeRequest?.host ? { writeHost: writeRequest.host } : {}),
|
|
989
1017
|
};
|
|
990
1018
|
if (args.json) {
|
|
991
1019
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -994,7 +1022,12 @@ async function main() {
|
|
|
994
1022
|
for (const relativePath of missing) {
|
|
995
1023
|
console.error(` - ${relativePath}`);
|
|
996
1024
|
}
|
|
997
|
-
|
|
1025
|
+
const installArgs = writeRequest?.host
|
|
1026
|
+
? `--install-agent-gates --tools ${writeRequest.host} --require-write-hook ${writeRequest.host}`
|
|
1027
|
+
: '--install-agent-gates';
|
|
1028
|
+
console.error(
|
|
1029
|
+
`\nRun \`${arkCommand(args.root, 'ark', 'init')}\` (or \`${arkCommand(args.root, 'ark-check', installArgs)}\`) to configure enforcement.`
|
|
1030
|
+
);
|
|
998
1031
|
}
|
|
999
1032
|
process.exitCode = 1;
|
|
1000
1033
|
return;
|
|
@@ -1004,7 +1037,12 @@ async function main() {
|
|
|
1004
1037
|
// When --require-gates is the only intent (no config/architecture run needed),
|
|
1005
1038
|
// callers still get a clear signal from the exit code and the human-mode line.
|
|
1006
1039
|
if (!args.json) {
|
|
1007
|
-
|
|
1040
|
+
if (args.requireGates) {
|
|
1041
|
+
console.log('Ark gates present (merge profile): ' + REQUIRED_GATE_FILES.join(', '));
|
|
1042
|
+
}
|
|
1043
|
+
if (writeRequest?.host) {
|
|
1044
|
+
console.log(`Ark hard-write hook present for ${writeRequest.host}.`);
|
|
1045
|
+
}
|
|
1008
1046
|
}
|
|
1009
1047
|
}
|
|
1010
1048
|
|
|
@@ -1277,6 +1315,18 @@ async function main() {
|
|
|
1277
1315
|
}
|
|
1278
1316
|
}
|
|
1279
1317
|
}
|
|
1318
|
+
|
|
1319
|
+
// Best-effort: open the report in the default browser. If it opens, fine;
|
|
1320
|
+
// if not (headless, no GUI, spawn error), do nothing — never fail the check.
|
|
1321
|
+
// Skipped in CI / Vitest / ARK_NO_OPEN_REPORT; --open / --no-open override.
|
|
1322
|
+
if (
|
|
1323
|
+
shouldOpenHtmlReport({
|
|
1324
|
+
force: Boolean(args.openReport),
|
|
1325
|
+
noOpen: Boolean(args.noOpenReport) || Boolean(args.json),
|
|
1326
|
+
})
|
|
1327
|
+
) {
|
|
1328
|
+
openHtmlInBrowser(reportPath);
|
|
1329
|
+
}
|
|
1280
1330
|
}
|
|
1281
1331
|
|
|
1282
1332
|
if (args.json) {
|
|
@@ -1385,10 +1435,19 @@ async function main() {
|
|
|
1385
1435
|
const parts = [];
|
|
1386
1436
|
if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
|
|
1387
1437
|
if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale} outdated`);
|
|
1438
|
+
// Advisory always; when session host is known and not Codex, say so so
|
|
1439
|
+
// /ark-upgrade does not chase home prompts as Incomplete.
|
|
1440
|
+
const activeHost = detectActiveAgentHost();
|
|
1441
|
+
const deferredNote =
|
|
1442
|
+
activeHost != null && activeHost !== 'codex'
|
|
1443
|
+
? ' Deferred unless you use Codex — not a blocker for Grok/Claude/Cursor. '
|
|
1444
|
+
: ' ';
|
|
1388
1445
|
console.log(
|
|
1389
1446
|
color.dim(
|
|
1390
|
-
`/ark-* skills in ${codexPromptsDir()} are behind this Ark (${parts.join(', ')})
|
|
1391
|
-
|
|
1447
|
+
`/ark-* skills in ${codexPromptsDir()} are behind this Ark (${parts.join(', ')}).` +
|
|
1448
|
+
deferredNote +
|
|
1449
|
+
`Codex loads them from $CODEX_HOME/prompts, not the repo. ` +
|
|
1450
|
+
`When using Codex: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --codex-home --force')}`
|
|
1392
1451
|
)
|
|
1393
1452
|
);
|
|
1394
1453
|
}
|
package/bin/ark-mcp.mjs
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { createImportTargetResolver } from './lib/import-resolve.mjs';
|
|
59
59
|
import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
|
|
60
60
|
import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
61
|
+
import { loadArkConfigContract } from './lib/config-contract.mjs';
|
|
61
62
|
|
|
62
63
|
const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
|
|
63
64
|
|
|
@@ -120,6 +121,11 @@ function readJson(file, { required } = {}) {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
function readArkConfig(file, { required } = {}) {
|
|
125
|
+
const raw = readJson(file, { required });
|
|
126
|
+
return raw === undefined ? undefined : loadArkConfigContract(raw, file).config;
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
function resolveInRoot(root, maybePath) {
|
|
124
130
|
if (!maybePath) return undefined;
|
|
125
131
|
return path.isAbsolute(maybePath) ? maybePath : path.join(root, maybePath);
|
|
@@ -460,11 +466,11 @@ async function main() {
|
|
|
460
466
|
const ts = await loadOptionalTypeScript();
|
|
461
467
|
|
|
462
468
|
const config =
|
|
463
|
-
(configPath ?
|
|
464
|
-
|
|
465
|
-
layers: [],
|
|
466
|
-
|
|
467
|
-
|
|
469
|
+
(configPath ? readArkConfig(configPath, { required: args.configExplicit }) : undefined) ??
|
|
470
|
+
loadArkConfigContract(
|
|
471
|
+
{ include: ['src'], layers: [], rules: DEFAULT_RULES },
|
|
472
|
+
configPath ?? 'ark.config.json'
|
|
473
|
+
).config;
|
|
468
474
|
if (!config.layers || config.layers.length === 0) {
|
|
469
475
|
process.stderr.write(
|
|
470
476
|
'[ark-mcp] warning: no layers configured — file→layer inference from config patterns ' +
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_ARK_CONFIG_RULES,
|
|
6
|
+
withArkConfigMetadata,
|
|
7
|
+
} from './lib/config-contract.mjs';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
* Default
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* CLIs run standalone with only `typescript` present, no build step.
|
|
10
|
+
* Default intent-prefix map shared by both CLIs and the ark-mcp write-path gate. The rule
|
|
11
|
+
* matrix comes from the generated Domain config contract above. Prefixes mirror the runtime
|
|
12
|
+
* profile but stay in this standalone CLI module because the CLIs run without a build step.
|
|
10
13
|
*/
|
|
11
14
|
export const DEFAULT_INTENT_PREFIXES = [
|
|
12
15
|
{ layer: 'DomainModel', prefixes: ['Domain.'] },
|
|
@@ -41,35 +44,7 @@ export const DEFAULT_LAYER_DIRECTORIES = {
|
|
|
41
44
|
Kernel: ['kernel'],
|
|
42
45
|
};
|
|
43
46
|
|
|
44
|
-
const
|
|
45
|
-
{ from: 'PresentationAdapters', to: 'ApplicationOrchestration' },
|
|
46
|
-
{ from: 'ApplicationOrchestration', to: 'DomainModel' },
|
|
47
|
-
{ from: 'WorkflowSagaEngine', to: 'ApplicationOrchestration' },
|
|
48
|
-
{ from: 'WorkflowSagaEngine', to: 'DomainModel' },
|
|
49
|
-
{ from: 'BackgroundJobsScheduling', to: 'ApplicationOrchestration' },
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
function flowKey(from, to) {
|
|
53
|
-
return `${from}->${to}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function createStrictDenyRules(layers, allowedFlows) {
|
|
57
|
-
const allowed = new Set(allowedFlows.map((flow) => flowKey(flow.from, flow.to)));
|
|
58
|
-
const rules = [];
|
|
59
|
-
for (const from of layers) {
|
|
60
|
-
for (const to of layers) {
|
|
61
|
-
if (from.layer === to.layer) continue;
|
|
62
|
-
if (allowed.has(flowKey(from.layer, to.layer))) continue;
|
|
63
|
-
rules.push({ from: from.layer, to: to.layer, allowed: false });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return rules;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const DEFAULT_RULES = createStrictDenyRules(
|
|
70
|
-
DEFAULT_INTENT_PREFIXES,
|
|
71
|
-
DEFAULT_ALLOWED_FLOWS
|
|
72
|
-
);
|
|
47
|
+
export const DEFAULT_RULES = DEFAULT_ARK_CONFIG_RULES;
|
|
73
48
|
|
|
74
49
|
/**
|
|
75
50
|
* Default ambient globals forbidden in the domain layer: a pure domain does no I/O and is
|
|
@@ -82,7 +57,7 @@ export function createElevenLayerConfig(options = {}) {
|
|
|
82
57
|
const rootDir = options.rootDir ?? 'src';
|
|
83
58
|
const optional = options.optionalLayers ?? true;
|
|
84
59
|
const prefix = rootDir === '.' ? '' : `${rootDir}/`;
|
|
85
|
-
const config = {
|
|
60
|
+
const config = withArkConfigMetadata({
|
|
86
61
|
include: options.include ?? [rootDir],
|
|
87
62
|
layers: DEFAULT_INTENT_PREFIXES.map((entry) => ({
|
|
88
63
|
name: entry.layer,
|
|
@@ -96,7 +71,7 @@ export function createElevenLayerConfig(options = {}) {
|
|
|
96
71
|
: {}),
|
|
97
72
|
})),
|
|
98
73
|
rules: DEFAULT_RULES,
|
|
99
|
-
};
|
|
74
|
+
});
|
|
100
75
|
// When a project root is known, overlay Nest/Next/express filename conventions so a
|
|
101
76
|
// flat framework starter is governed on day one (not "0% governed / false green").
|
|
102
77
|
if (options.root) return applyFrameworkLayoutOverlays(config, options.root);
|
|
@@ -134,7 +109,7 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
134
109
|
try {
|
|
135
110
|
signals = collectRepoShapeSignals(root);
|
|
136
111
|
} catch {
|
|
137
|
-
return config;
|
|
112
|
+
return withArkConfigMetadata(config);
|
|
138
113
|
}
|
|
139
114
|
|
|
140
115
|
const next = {
|
|
@@ -344,7 +319,7 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
344
319
|
: 'library';
|
|
345
320
|
}
|
|
346
321
|
|
|
347
|
-
return next;
|
|
322
|
+
return withArkConfigMetadata(next);
|
|
348
323
|
}
|
|
349
324
|
|
|
350
325
|
/**
|
|
@@ -398,40 +373,97 @@ export function resolveOperatingMode({
|
|
|
398
373
|
return 'suggest';
|
|
399
374
|
}
|
|
400
375
|
|
|
376
|
+
function singleFileTypeChecker(ts, sourceFile) {
|
|
377
|
+
const options = {
|
|
378
|
+
noLib: true,
|
|
379
|
+
noResolve: true,
|
|
380
|
+
target: ts.ScriptTarget.Latest,
|
|
381
|
+
};
|
|
382
|
+
const host = ts.createCompilerHost(options, true);
|
|
383
|
+
host.getSourceFile = (fileName) =>
|
|
384
|
+
fileName === sourceFile.fileName ? sourceFile : undefined;
|
|
385
|
+
host.fileExists = (fileName) => fileName === sourceFile.fileName;
|
|
386
|
+
host.readFile = (fileName) =>
|
|
387
|
+
fileName === sourceFile.fileName ? sourceFile.text : undefined;
|
|
388
|
+
return ts.createProgram([sourceFile.fileName], options, host).getTypeChecker();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function propertyAccessPath(ts, node) {
|
|
392
|
+
const segments = [];
|
|
393
|
+
let current = node;
|
|
394
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
395
|
+
segments.unshift(current.name.text);
|
|
396
|
+
current = current.expression;
|
|
397
|
+
}
|
|
398
|
+
if (!ts.isIdentifier(current)) return undefined;
|
|
399
|
+
segments.unshift(current.text);
|
|
400
|
+
return { root: current, segments };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isRuntimeIdentifierReference(ts, node) {
|
|
404
|
+
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) return false;
|
|
405
|
+
return (
|
|
406
|
+
(ts.isExpressionNode(node) && !ts.isInTypeQuery(node)) ||
|
|
407
|
+
(ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function hasLocalDeclaration(ts, checker, sourceFile, node) {
|
|
412
|
+
const shorthand =
|
|
413
|
+
ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node;
|
|
414
|
+
const symbol = shorthand
|
|
415
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
416
|
+
: checker.getSymbolAtLocation(node);
|
|
417
|
+
return Boolean(
|
|
418
|
+
symbol?.declarations?.some((declaration) => declaration.getSourceFile() === sourceFile)
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
401
422
|
/**
|
|
402
423
|
* Find uses of forbidden ambient globals in a TypeScript source file.
|
|
403
424
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* - a bare entry ("console", "fetch") flags property accesses on it (`console.log`),
|
|
409
|
-
* direct calls (`fetch(...)`), and constructions (`new WebSocket(...)`)
|
|
410
|
-
* Bare identifier mentions in other positions (types, shadowed locals, import names) are
|
|
411
|
-
* NOT flagged, trading a little recall for near-zero false positives without a type checker.
|
|
425
|
+
* A no-lib, no-resolution TypeScript program binds declarations in this file only. An
|
|
426
|
+
* identifier with a symbol is therefore local (parameter, variable, import, etc.); an
|
|
427
|
+
* unbound runtime identifier is ambient. Dotted entries use AST property chains and
|
|
428
|
+
* explicit `globalThis` access is normalized to the configured global name.
|
|
412
429
|
*
|
|
430
|
+
* Kept in sync with `analyzeForbiddenGlobals` in
|
|
431
|
+
* src/kernel/ai-gate/AICodeGate.ts — the standalone CLIs must not import from dist.
|
|
413
432
|
* Returns [{ name, node }] where `name` is the matched forbidden entry.
|
|
414
433
|
*/
|
|
415
434
|
export function collectForbiddenGlobalUses(ts, sourceFile, forbidden) {
|
|
416
435
|
const entries = new Set(forbidden ?? []);
|
|
417
436
|
if (entries.size === 0) return [];
|
|
437
|
+
const checker = singleFileTypeChecker(ts, sourceFile);
|
|
418
438
|
const uses = [];
|
|
419
439
|
|
|
420
440
|
const visit = (node) => {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
441
|
+
const nestedPropertyAccess =
|
|
442
|
+
ts.isPropertyAccessExpression(node) &&
|
|
443
|
+
ts.isPropertyAccessExpression(node.parent) &&
|
|
444
|
+
node.parent.expression === node;
|
|
445
|
+
if (ts.isPropertyAccessExpression(node) && !nestedPropertyAccess) {
|
|
446
|
+
const path = propertyAccessPath(ts, node);
|
|
447
|
+
if (path && !hasLocalDeclaration(ts, checker, sourceFile, path.root)) {
|
|
448
|
+
const explicitGlobalThis = path.segments[0] === 'globalThis';
|
|
449
|
+
const normalized = explicitGlobalThis ? path.segments.slice(1) : path.segments;
|
|
450
|
+
let match;
|
|
451
|
+
for (let length = normalized.length; length >= (explicitGlobalThis ? 1 : 2); length -= 1) {
|
|
452
|
+
const candidate = normalized.slice(0, length).join('.');
|
|
453
|
+
if (entries.has(candidate)) {
|
|
454
|
+
match = candidate;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (match) uses.push({ name: match, node });
|
|
427
459
|
}
|
|
428
460
|
} else if (
|
|
429
|
-
|
|
430
|
-
node.
|
|
431
|
-
ts
|
|
432
|
-
|
|
461
|
+
ts.isIdentifier(node) &&
|
|
462
|
+
entries.has(node.text) &&
|
|
463
|
+
isRuntimeIdentifierReference(ts, node) &&
|
|
464
|
+
!hasLocalDeclaration(ts, checker, sourceFile, node)
|
|
433
465
|
) {
|
|
434
|
-
uses.push({ name: node.
|
|
466
|
+
uses.push({ name: node.text, node });
|
|
435
467
|
}
|
|
436
468
|
ts.forEachChild(node, visit);
|
|
437
469
|
};
|