arkgate 3.6.1 → 3.8.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 +122 -1145
- package/README.md +59 -19
- package/bin/ark-check-runtime.mjs +1598 -0
- package/bin/ark-check.mjs +32 -1565
- package/bin/ark-layer-match.mjs +2 -1
- package/bin/ark-mcp-runtime.mjs +1976 -0
- package/bin/ark-mcp.mjs +84 -1495
- package/bin/ark-shared.mjs +34 -38
- package/bin/ark.mjs +33 -66
- package/bin/lib/adapter-contract.mjs +161 -9
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/analysis-completeness.mjs +28 -0
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/analysis-policy.mjs +27 -0
- package/bin/lib/architecture-scan.mjs +70 -304
- package/bin/lib/auto-patch.mjs +76 -8
- package/bin/lib/ci-and-commands.mjs +1 -1
- package/bin/lib/codex-home.mjs +43 -16
- package/bin/lib/design-delta.mjs +4 -0
- package/bin/lib/design-smells.mjs +67 -14
- package/bin/lib/doctor-advisories.mjs +23 -7
- package/bin/lib/doctor-plan.mjs +44 -47
- package/bin/lib/enforcement-state.mjs +2 -0
- package/bin/lib/github-enforcement.mjs +443 -0
- package/bin/lib/hook-templates.mjs +12 -148
- package/bin/lib/html-report-advisories.mjs +59 -0
- package/bin/lib/html-report-depth.mjs +9 -0
- package/bin/lib/html-report.mjs +5 -5
- package/bin/lib/install-migrate.mjs +83 -79
- package/bin/lib/managed-upgrade.mjs +622 -0
- package/bin/lib/mcp-adoption.mjs +3 -1
- package/bin/lib/parse-health.mjs +75 -0
- package/bin/lib/port-proof.mjs +2 -2
- package/bin/lib/prepare-change.mjs +68 -38
- package/bin/lib/prepare-write.mjs +7 -1
- package/bin/lib/reshape-decisions.mjs +284 -0
- package/bin/lib/resident-doctor-client.mjs +55 -0
- package/bin/lib/resident-hook.mjs +247 -0
- package/bin/lib/resolved-candidate-facts.mjs +1160 -0
- package/bin/lib/scan-files.mjs +19 -6
- package/bin/lib/snippet-analysis.mjs +119 -0
- package/bin/lib/source-policy.mjs +24 -0
- package/bin/lib/typescript-host.mjs +15 -18
- package/bin/lib/unavailable-analysis.mjs +76 -0
- package/bin/lib/upgrade-command.mjs +115 -0
- package/bin/lib/weakest-link.mjs +21 -179
- package/bin/lib/write-path-capabilities.mjs +167 -16
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +3 -0
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +7 -7
- package/dist/index.d.ts +1073 -141
- package/dist/index.js +7 -7
- package/docs/agent-guide.md +127 -52
- package/docs/ai-gates.md +100 -18
- package/docs/configuration.md +6 -0
- package/docs/demos/01-write-gate-self-correction.md +2 -2
- package/docs/enthusiast/README.md +10 -10
- package/docs/enthusiast/how-to-gallery-starter.md +2 -2
- package/docs/enthusiast/reference-commands.md +18 -1
- package/docs/enthusiast/tutorial-first-project.md +2 -2
- package/docs/package-surface.md +101 -14
- package/docs/typescript-support.md +118 -37
- package/package.json +33 -4
- package/schemas/ark.analysis-result.schema.json +159 -2
- package/schemas/ark.design-delta.schema.json +1 -0
- package/schemas/ark.enforcement-state.schema.json +84 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +12 -0
- package/templates/skills/ark-explore.md +12 -5
- package/templates/skills/ark-fix.md +12 -2
- package/templates/skills/ark-loop.md +14 -1
- package/templates/skills/ark-runtime.md +15 -8
- package/templates/skills/ark-upgrade.md +122 -182
- package/bin/lib/ai-velocity.mjs +0 -293
- package/bin/lib/graph-cycles.mjs +0 -6
- package/bin/lib/safety-diagnostics.mjs +0 -284
- package/bin/lib/ts-resolve.mjs +0 -227
- package/dist/configTypes-DAPvBqK6.d.cts +0 -61
- package/dist/eslint/index.d.cts +0 -146
- package/dist/index.d.cts +0 -986
package/bin/ark-check.mjs
CHANGED
|
@@ -1,1589 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
2
|
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
3
|
|
|
7
|
-
|
|
4
|
+
import { tryResidentDoctor } from './lib/resident-doctor-client.mjs';
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
ADOPTION_PLAN_FILENAME,
|
|
17
|
-
buildArchitectureRecommendation,
|
|
18
|
-
createElevenLayerConfig,
|
|
19
|
-
enrichViolationWithFixClass,
|
|
20
|
-
listPolicyPackIds,
|
|
21
|
-
loadPolicyPackMeta,
|
|
22
|
-
writeAdoptionPlan,
|
|
23
|
-
detectWorkspaces,
|
|
24
|
-
detectTsPackageRoots,
|
|
25
|
-
resolveIncludeRoots,
|
|
26
|
-
formatArchitectureRecommendationHuman,
|
|
27
|
-
installDevHint,
|
|
28
|
-
layerForFile,
|
|
29
|
-
} from './ark-shared.mjs';
|
|
6
|
+
const VALUE_FLAGS = new Map([
|
|
7
|
+
['--root', 'root'],
|
|
8
|
+
['--config', 'config'],
|
|
9
|
+
['--manifest', 'manifest'],
|
|
10
|
+
['--tsconfig', 'tsconfig'],
|
|
11
|
+
]);
|
|
12
|
+
const BOOLEAN_FLAGS = new Set(['--resident', '--doctor', '--json', '--no-cache']);
|
|
30
13
|
|
|
31
|
-
|
|
32
|
-
runInstallAgentGates,
|
|
33
|
-
loadTypeScript,
|
|
34
|
-
detectSkillGaps,
|
|
35
|
-
detectCodexHomeGap,
|
|
36
|
-
detectCodexRepoSkillGap,
|
|
37
|
-
codexConcernIsActive,
|
|
38
|
-
printSkillAndCodexGapHints,
|
|
39
|
-
missingGates,
|
|
40
|
-
staleRunnerGateFiles,
|
|
41
|
-
brokenMcpGateFiles,
|
|
42
|
-
readJson,
|
|
43
|
-
hasCheckArchitectureScript,
|
|
44
|
-
checkArchitectureScriptSnippet,
|
|
45
|
-
arkCheckCommand,
|
|
46
|
-
arkPackageVersion,
|
|
47
|
-
compactRouterHost,
|
|
48
|
-
REQUIRED_GATE_FILES,
|
|
49
|
-
detectWritePathCapabilities,
|
|
50
|
-
} from './lib/agent-gates.mjs';
|
|
51
|
-
import { syncBaselineIntoCheckSurfaces } from './lib/field-install.mjs';
|
|
52
|
-
import {
|
|
53
|
-
detectEnforcement,
|
|
54
|
-
renderHtmlReport,
|
|
55
|
-
renderBeginnerHtmlReport,
|
|
56
|
-
archiveReportSnapshots,
|
|
57
|
-
buildReportSnapshot,
|
|
58
|
-
computeReportFitness,
|
|
59
|
-
reportsDir,
|
|
60
|
-
readJsonSafe,
|
|
61
|
-
} from './lib/html-report.mjs';
|
|
62
|
-
import { computeDoctorAdvisories } from './lib/doctor-advisories.mjs';
|
|
63
|
-
import { buildReportDepthPayload } from './lib/html-report-depth.mjs';
|
|
64
|
-
import { shouldOpenHtmlReport, openHtmlInBrowser } from './lib/open-html.mjs';
|
|
65
|
-
import {
|
|
66
|
-
computeCoverage,
|
|
67
|
-
runCoverage,
|
|
68
|
-
runPlan,
|
|
69
|
-
runDoctor,
|
|
70
|
-
} from './lib/doctor-plan.mjs';
|
|
71
|
-
import { runRatchetCores } from './lib/core-ratchet.mjs';
|
|
72
|
-
import {
|
|
73
|
-
baselineKey,
|
|
74
|
-
baselineOccurrenceKeys,
|
|
75
|
-
readBaseline,
|
|
76
|
-
summarizeViolations,
|
|
77
|
-
writeBaseline,
|
|
78
|
-
printViolation,
|
|
79
|
-
printViolationBreakdown,
|
|
80
|
-
CONCENTRATION_MIN_VIOLATIONS,
|
|
81
|
-
} from './lib/violations.mjs';
|
|
82
|
-
import {
|
|
83
|
-
suggestLayerForDir,
|
|
84
|
-
detectBestFitModel,
|
|
85
|
-
dirSegmentsFromGlob,
|
|
86
|
-
} from './lib/suggestions.mjs';
|
|
87
|
-
import {
|
|
88
|
-
ARCHITECTURE_PRESETS,
|
|
89
|
-
} from './lib/presets.mjs';
|
|
90
|
-
import { loadArkConfigContract, parseArkConfigJson } from './lib/config-contract.mjs';
|
|
91
|
-
import { createAdapterResult } from './lib/adapter-contract.mjs';
|
|
92
|
-
|
|
93
|
-
import {
|
|
94
|
-
collectGovernedFiles,
|
|
95
|
-
normalize,
|
|
96
|
-
walk,
|
|
97
|
-
} from './lib/scan-files.mjs';
|
|
98
|
-
import {
|
|
99
|
-
configWarning,
|
|
100
|
-
} from './lib/config-warnings.mjs';
|
|
101
|
-
import { runArchitectureScan } from './lib/architecture-scan.mjs';
|
|
102
|
-
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
103
|
-
import { analyzePolicyTransition } from './lib/policy-delta-io.mjs';
|
|
104
|
-
|
|
105
|
-
function parseArgs(argv) {
|
|
14
|
+
function residentArgs(argv) {
|
|
106
15
|
const args = {
|
|
107
16
|
root: process.cwd(),
|
|
108
17
|
config: 'ark.config.json',
|
|
109
18
|
manifest: undefined,
|
|
110
|
-
printConfig: undefined,
|
|
111
19
|
tsconfig: undefined,
|
|
112
|
-
|
|
113
|
-
strictConfig: false,
|
|
114
|
-
strictMerge: false,
|
|
115
|
-
requireGates: false,
|
|
116
|
-
requireWriteHook: undefined,
|
|
117
|
-
init: false,
|
|
118
|
-
installAgentGates: false,
|
|
119
|
-
compact: false,
|
|
120
|
-
tools: undefined,
|
|
121
|
-
force: false,
|
|
122
|
-
skillsOnly: false,
|
|
123
|
-
baseline: undefined,
|
|
124
|
-
policyBase: undefined,
|
|
125
|
-
policyBaseRef: undefined,
|
|
126
|
-
policyAck: undefined,
|
|
127
|
-
updateBaseline: false,
|
|
128
|
-
noCache: false,
|
|
129
|
-
coverage: false,
|
|
130
|
-
migrateCommands: false,
|
|
20
|
+
resident: false,
|
|
131
21
|
doctor: false,
|
|
132
|
-
|
|
133
|
-
recommend: false,
|
|
134
|
-
writePlan: false,
|
|
135
|
-
listPolicyPacks: false,
|
|
136
|
-
applyPolicyPack: undefined,
|
|
137
|
-
watch: false,
|
|
138
|
-
beginner: false,
|
|
139
|
-
openReport: false,
|
|
140
|
-
noOpenReport: false,
|
|
141
|
-
version: false,
|
|
142
|
-
help: false,
|
|
143
|
-
};
|
|
144
|
-
const requireValue = (flag, index) => {
|
|
145
|
-
const value = argv[index + 1];
|
|
146
|
-
if (value === undefined || value.startsWith('-')) {
|
|
147
|
-
throw new Error(`Missing value for ${flag}. Run ark-check --help for usage.`);
|
|
148
|
-
}
|
|
149
|
-
return value;
|
|
22
|
+
json: false,
|
|
150
23
|
};
|
|
151
|
-
for (let
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
args
|
|
158
|
-
|
|
159
|
-
else if (arg === '--strict-config') args.strictConfig = true;
|
|
160
|
-
else if (arg === '--require-gates') args.requireGates = true;
|
|
161
|
-
else if (arg === '--require-write-hook') {
|
|
162
|
-
args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
|
|
163
|
-
}
|
|
164
|
-
else if (arg === '--init') args.init = true;
|
|
165
|
-
else if (arg === '--preset') args.preset = requireValue(arg, i++);
|
|
166
|
-
else if (arg === '--install-agent-gates') args.installAgentGates = true;
|
|
167
|
-
else if (arg === '--compact') args.compact = true;
|
|
168
|
-
else if (arg === '--tools') {
|
|
169
|
-
// Consume the next arg only when it isn't another flag (same rule as --baseline),
|
|
170
|
-
// so `--tools --force` can't silently eat --force as a "tool name".
|
|
171
|
-
const next = argv[i + 1];
|
|
172
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
173
|
-
i += 1;
|
|
174
|
-
args.tools = next
|
|
175
|
-
.split(',')
|
|
176
|
-
.map((tool) => tool.trim().toLowerCase())
|
|
177
|
-
.filter(Boolean);
|
|
178
|
-
} else {
|
|
179
|
-
args.tools = []; // flag without a value — rejected in runInstallAgentGates
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
else if (arg === '--force') args.force = true;
|
|
183
|
-
else if (arg === '--skills-only') args.skillsOnly = true;
|
|
184
|
-
else if (arg === '--coverage') args.coverage = true;
|
|
185
|
-
else if (arg === '--doctor') args.doctor = true;
|
|
186
|
-
else if (arg === '--plan') args.plan = true;
|
|
187
|
-
else if (arg === '--recommend') args.recommend = true;
|
|
188
|
-
else if (arg === '--write-plan') args.writePlan = true;
|
|
189
|
-
else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
|
|
190
|
-
else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++);
|
|
191
|
-
else if (arg === '--suggest-include') args.suggestInclude = true;
|
|
192
|
-
else if (arg === '--adopt-contract') args.adoptContract = true;
|
|
193
|
-
else if (arg === '--ratchet-cores') args.ratchetCores = true;
|
|
194
|
-
else if (arg === '--write') args.write = true;
|
|
195
|
-
else if (arg === '--watch') args.watch = true;
|
|
196
|
-
else if (arg === '--beginner') args.beginner = true;
|
|
197
|
-
else if (arg === '--codex-home') args.codexHome = true;
|
|
198
|
-
else if (arg === '--migrate-commands') args.migrateCommands = true;
|
|
199
|
-
else if (arg === '--no-cache') args.noCache = true;
|
|
200
|
-
else if (arg === '--report') {
|
|
201
|
-
const next = argv[i + 1];
|
|
202
|
-
args.report = next && !next.startsWith('-') ? argv[++i] : 'ark-report.html';
|
|
203
|
-
}
|
|
204
|
-
else if (arg === '--reset-origin') args.resetOrigin = true;
|
|
205
|
-
else if (arg === '--no-archive') args.noArchive = true;
|
|
206
|
-
else if (arg === '--open') args.openReport = true;
|
|
207
|
-
else if (arg === '--no-open') args.noOpenReport = true;
|
|
208
|
-
else if (arg === '--baseline' || arg === '--update-baseline') {
|
|
209
|
-
if (arg === '--update-baseline') args.updateBaseline = true;
|
|
210
|
-
// optional path value: consume the next arg only when it isn't another flag
|
|
211
|
-
const next = argv[i + 1];
|
|
212
|
-
args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
|
|
213
|
-
}
|
|
214
|
-
else if (arg === '--policy-base') args.policyBase = requireValue(arg, i++);
|
|
215
|
-
else if (arg === '--policy-base-ref') args.policyBaseRef = requireValue(arg, i++);
|
|
216
|
-
else if (arg === '--policy-ack') args.policyAck = requireValue(arg, i++);
|
|
217
|
-
else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++));
|
|
218
|
-
else if (arg === '--config') args.config = requireValue(arg, i++);
|
|
219
|
-
else if (arg === '--manifest') args.manifest = requireValue(arg, i++);
|
|
220
|
-
else if (arg === '--print-config') args.printConfig = requireValue(arg, i++);
|
|
221
|
-
else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++);
|
|
222
|
-
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
223
|
-
else if (arg === '--version' || arg === '-V') args.version = true;
|
|
224
|
-
else throw new Error(`Unknown argument: ${arg}. Run ark-check --help for usage.`);
|
|
225
|
-
}
|
|
226
|
-
return args;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Path shown to humans: project-relative when inside root, absolute otherwise (no `../../..`). */
|
|
230
|
-
function displayPathFromRoot(root, absPath) {
|
|
231
|
-
const rel = path.relative(root, absPath);
|
|
232
|
-
if (!rel || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
233
|
-
return absPath;
|
|
234
|
-
}
|
|
235
|
-
return rel.split(path.sep).join('/');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function usage() {
|
|
239
|
-
return [
|
|
240
|
-
'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
|
|
241
|
-
' ark-check --version',
|
|
242
|
-
' ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--policy-base <file> | --policy-base-ref <git-ref>] [--policy-ack <file>] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
243
|
-
' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
|
|
244
|
-
' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
|
|
245
|
-
' ark-check --recommend [--json] [--write-plan] application-shape plan; --write-plan emits ark-adoption-plan.json',
|
|
246
|
-
' ark-check --list-policy-packs enthusiast packs (hexagonal, layered, feature-sliced, monorepo, ui-surface, vertical-slice, ddd-bounded-contexts)',
|
|
247
|
-
' ark-check --apply-policy-pack <id> [--force] write ark.config.json from templates/policy-packs/ (uses preset factory)',
|
|
248
|
-
' ark-check --suggest-include [--json] propose include roots (TS packages / workspaces)',
|
|
249
|
-
' ark-check --adopt-contract [--write] expand include + UI patterns from ungoverned dirs (contract adopt)',
|
|
250
|
-
' ark-check --ratchet-cores when raw graph is green (0 violations; baseline ignored), set optional:false on populated cores only (writes ark.config.json)',
|
|
251
|
-
' ark-check --watch re-run the check when governed files change (debounced)',
|
|
252
|
-
' ark-check --report [file.html] [--beginner] [--reset-origin] [--no-archive] [--open|--no-open]',
|
|
253
|
-
' HTML report + snapshots under .ark/reports/ (origin once, latest each run, history JSON)',
|
|
254
|
-
' Best-effort open in browser (local TTY). No-op if open fails. --no-open / ARK_NO_OPEN_REPORT=1 to skip; --open forces open.',
|
|
255
|
-
' ark-check --init [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|clean-architecture|onion-architecture] [--force]',
|
|
256
|
-
' ark-check --install-agent-gates [--tools claude,cursor,codex,grok] [--require-write-hook <host>] [--skills-only] [--codex-home] [--force]',
|
|
257
|
-
' ark-check --update-baseline [file] freeze current violations (default .ark-baseline.json)',
|
|
258
|
-
' ark-check --print-config eleven-layer',
|
|
259
|
-
'',
|
|
260
|
-
'Adopting Ark in an existing codebase? Run --update-baseline once to freeze existing',
|
|
261
|
-
'violations, commit the baseline file, and gate CI with --baseline: only NEW violations',
|
|
262
|
-
'fail the check, so the ratchet only moves toward zero.',
|
|
263
|
-
'',
|
|
264
|
-
'--init scans the project for the built-in layer directory conventions (src/domain,',
|
|
265
|
-
'src/application, src/adapters/persistence, ...) and writes an ark.config.json covering',
|
|
266
|
-
'only the layers that actually exist, with the default rules filtered to those layers.',
|
|
267
|
-
'Undetected profile layers are printed as suggestions with their conventional',
|
|
268
|
-
'directories. When nothing is detected, the full 11-layer starter profile is written',
|
|
269
|
-
'instead (all layers optional, anchored at src/), so the strict check passes today and',
|
|
270
|
-
'each layer starts being enforced as soon as its directory gains source files.',
|
|
271
|
-
'',
|
|
272
|
-
'Resolves relative, tsconfig path-alias, and package imports via the TypeScript',
|
|
273
|
-
'module resolver, then checks each resolved cross-layer import against the rules.',
|
|
274
|
-
'Path aliases resolve against the NEAREST tsconfig.json above each source file, so',
|
|
275
|
-
'monorepo packages with per-package configs work under a single --root. Pass',
|
|
276
|
-
'--tsconfig to force one config for every file. If no tsconfig is found, path',
|
|
277
|
-
'aliases are unavailable but relative/package imports still resolve.',
|
|
278
|
-
'',
|
|
279
|
-
'Parsed files are cached in node_modules/.cache/ark-check.json (keyed by mtime+size',
|
|
280
|
-
'and the config/manifest contents); import edges are always re-resolved against the',
|
|
281
|
-
'live filesystem, so the cache can never hide a new violation. --no-cache disables it.',
|
|
282
|
-
'',
|
|
283
|
-
'Config shape:',
|
|
284
|
-
'{',
|
|
285
|
-
' "include": ["src"],',
|
|
286
|
-
' // optional: "exclude": ["**/vendor/**"], "excludeGenerated": false (default skips *.gen.ts / *.generated.ts)',
|
|
287
|
-
' "layers": [',
|
|
288
|
-
' { "name": "DomainModel", "patterns": ["src/domain/**"], "intentPrefixes": ["Domain."],',
|
|
289
|
-
' "forbiddenGlobals": ["fetch", "process", "Date.now", "Math.random"] }',
|
|
290
|
-
' ],',
|
|
291
|
-
' "rules": [{ "from": "DomainModel", "to": "PersistenceAdapters", "allowed": false }]',
|
|
292
|
-
'}',
|
|
293
|
-
'',
|
|
294
|
-
'Config warnings are advisory by default and are included in JSON output.',
|
|
295
|
-
'Use --strict-config to make config warnings fail the check.',
|
|
296
|
-
'Use --strict-merge for the fail-closed CI profile: --strict-config + --require-gates',
|
|
297
|
-
'plus the security diagnostics surfaced by doctor. --strict is a compatibility alias.',
|
|
298
|
-
'This merge profile never depends on an editor/agent hook.',
|
|
299
|
-
'When a Git merge base is available, --strict-merge classifies the ark.config.json',
|
|
300
|
-
'transition. Weakening or judgment-required findings fail unless --policy-ack names',
|
|
301
|
-
'every finding and is bound to both policy hashes. Use --policy-base/--policy-base-ref',
|
|
302
|
-
'for an explicit comparison; ARK_POLICY_BASE_REF is the CI environment equivalent.',
|
|
303
|
-
'Add --require-write-hook claude|grok to validate a hard local write boundary for that',
|
|
304
|
-
'specific host. Cursor and Codex expose advisory MCP tools plus the shared CI check;',
|
|
305
|
-
'merge blocking requires repository policy to make that status required.',
|
|
306
|
-
'',
|
|
307
|
-
'--require-gates fails the check when AGENTS.md, .mcp.json, or the generated CI',
|
|
308
|
-
'workflow is missing, so "installed but never configured" is a red CI. Combine it',
|
|
309
|
-
'with --strict-config to enforce gate presence and architecture in one run.',
|
|
310
|
-
'',
|
|
311
|
-
'--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
|
|
312
|
-
'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok',
|
|
313
|
-
'(Claude/Grok hard-write hooks; Cursor/Codex advisory MCP; shared CI check for all) and',
|
|
314
|
-
'windsurf, cline, copilot, kiro, roo, continue, gemini',
|
|
315
|
-
'(instruction-tier rule files derived from the same contract).',
|
|
316
|
-
'It also installs the /ark-* skills shipped in templates/skills/ into each',
|
|
317
|
-
'detected tool\'s command location (.claude/skills/, .cursor/commands/,',
|
|
318
|
-
'.agents/skills/ (Codex REPO catalog), .grok/skills/, .windsurf/workflows/,',
|
|
319
|
-
'.clinerules/workflows/, .github/prompts/).',
|
|
320
|
-
'Kiro, Roo, Continue, and Gemini have no command mechanism and receive only their',
|
|
321
|
-
'rule file. Existing files are never overwritten without --force, so re-running',
|
|
322
|
-
'after an update only adds what is missing. --skills-only restricts the write to',
|
|
323
|
-
'just the /ark-* skills (safe to --force-refresh — it leaves a customized AGENTS.md,',
|
|
324
|
-
'settings, and CI workflow untouched).',
|
|
325
|
-
'Pass --tools to pick which tool configs to write; otherwise they are auto-detected',
|
|
326
|
-
'from their config directories (.claude/, .cursor/, .codex/, .grok/, .windsurf/,',
|
|
327
|
-
'.clinerules/, .kiro/, .roo/, .continue/, .gemini/; copilot is explicit-only).',
|
|
328
|
-
'claude+cursor+codex+grok are written when nothing is detected.',
|
|
329
|
-
'',
|
|
330
|
-
'Generate a starter 11-layer config:',
|
|
331
|
-
' ark-check --print-config eleven-layer > ark.config.json',
|
|
332
|
-
'',
|
|
333
|
-
'Install agent + CI enforcement templates:',
|
|
334
|
-
' ark-check --install-agent-gates',
|
|
335
|
-
].join('\n');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function readConfig(root, configPath) {
|
|
339
|
-
const fullPath = path.isAbsolute(configPath)
|
|
340
|
-
? configPath
|
|
341
|
-
: path.join(root, configPath);
|
|
342
|
-
if (!fs.existsSync(fullPath)) {
|
|
343
|
-
return loadArkConfigContract(
|
|
344
|
-
{ include: ['src'], layers: [], rules: DEFAULT_RULES },
|
|
345
|
-
fullPath
|
|
346
|
-
).config;
|
|
347
|
-
}
|
|
348
|
-
return parseArkConfigJson(fs.readFileSync(fullPath, 'utf8'), fullPath).config;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Infer an ark.config.json from the directories that actually exist in the project,
|
|
353
|
-
* using the same layer→directory conventions as the eleven-layer template. A directory
|
|
354
|
-
* only counts when it contains at least one source file, so an empty scaffold dir can't
|
|
355
|
-
* produce a layer whose pattern matches nothing (which --strict-config would fail).
|
|
356
|
-
*/
|
|
357
|
-
function detectConfig(root) {
|
|
358
|
-
const srcDir = fs.existsSync(path.join(root, 'src')) ? 'src' : '.';
|
|
359
|
-
const layers = [];
|
|
360
|
-
|
|
361
|
-
for (const entry of DEFAULT_INTENT_PREFIXES) {
|
|
362
|
-
const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter(
|
|
363
|
-
(directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0
|
|
364
|
-
);
|
|
365
|
-
if (directories.length === 0) continue;
|
|
366
|
-
layers.push({
|
|
367
|
-
name: entry.layer,
|
|
368
|
-
patterns: directories.map((directory) => `${normalize(path.join(srcDir, directory))}/**`),
|
|
369
|
-
intentPrefixes: entry.prefixes,
|
|
370
|
-
...(entry.layer === 'DomainModel'
|
|
371
|
-
? { forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS }
|
|
372
|
-
: {}),
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const names = new Set(layers.map((layer) => layer.name));
|
|
377
|
-
const rules = DEFAULT_RULES.filter((rule) => names.has(rule.from) && names.has(rule.to));
|
|
378
|
-
|
|
379
|
-
return { srcDir, config: { include: [srcDir], layers, rules } };
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Top-level directories under srcDir not covered by any detected layer pattern. */
|
|
383
|
-
function uncoveredDirectories(root, srcDir, layers) {
|
|
384
|
-
const base = path.join(root, srcDir);
|
|
385
|
-
if (!fs.existsSync(base)) return [];
|
|
386
|
-
return fs
|
|
387
|
-
.readdirSync(base, { withFileTypes: true })
|
|
388
|
-
.filter(
|
|
389
|
-
(entry) =>
|
|
390
|
-
entry.isDirectory() &&
|
|
391
|
-
entry.name !== 'node_modules' &&
|
|
392
|
-
entry.name !== 'dist' &&
|
|
393
|
-
!entry.name.startsWith('.')
|
|
394
|
-
)
|
|
395
|
-
.map((entry) => entry.name)
|
|
396
|
-
.filter((name) => {
|
|
397
|
-
const prefix = `${normalize(path.join(srcDir, name))}/`;
|
|
398
|
-
return !layers.some((layer) =>
|
|
399
|
-
layer.patterns.some((pattern) => pattern.startsWith(prefix))
|
|
400
|
-
);
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// detectWorkspaces: shared implementation in ark-shared.mjs (npm/pnpm/rush/lerna +
|
|
405
|
-
// conventional multi-package roots).
|
|
406
|
-
|
|
407
|
-
// Deny every "upward" edge for an ordered layer list (index 0 = outermost/top,
|
|
408
|
-
// which may import everything below it). Inner/lower layers must not import outer
|
|
409
|
-
// ones — the shared shape behind linear layered and feature-sliced layouts.
|
|
410
|
-
|
|
411
|
-
function proposeForUncovered(root, srcDir, layers) {
|
|
412
|
-
const proposals = [];
|
|
413
|
-
for (const top of uncoveredDirectories(root, srcDir, layers)) {
|
|
414
|
-
const direct = suggestLayerForDir(top);
|
|
415
|
-
if (direct) {
|
|
416
|
-
proposals.push({ dir: `${srcDir}/${top}`, ...direct });
|
|
24
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
25
|
+
const flag = argv[index];
|
|
26
|
+
const property = VALUE_FLAGS.get(flag);
|
|
27
|
+
if (property) {
|
|
28
|
+
const value = argv[index + 1];
|
|
29
|
+
if (value === undefined || value.startsWith('-')) return null;
|
|
30
|
+
args[property] = property === 'root' ? path.resolve(value) : value;
|
|
31
|
+
index += 1;
|
|
417
32
|
continue;
|
|
418
33
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
|
|
424
|
-
.map((e) => e.name);
|
|
425
|
-
} catch {
|
|
426
|
-
/* not a readable directory — treat as unrecognized below */
|
|
427
|
-
}
|
|
428
|
-
if (children.length > 0) {
|
|
429
|
-
// Descend: propose per child so a mixed `lib/` yields lib/repositories → Persistence
|
|
430
|
-
// AND flags lib/db as unrecognized, instead of dropping the parts Ark can't place.
|
|
431
|
-
for (const child of children) {
|
|
432
|
-
const hit = suggestLayerForDir(child);
|
|
433
|
-
proposals.push(
|
|
434
|
-
hit
|
|
435
|
-
? { dir: `${srcDir}/${top}/${child}`, ...hit }
|
|
436
|
-
: { dir: `${srcDir}/${top}/${child}`, unrecognized: true }
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
} else {
|
|
440
|
-
proposals.push({ dir: `${srcDir}/${top}`, unrecognized: true });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return proposals;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function printInitNextSteps(root) {
|
|
447
|
-
console.log('');
|
|
448
|
-
console.log('Next steps:');
|
|
449
|
-
console.log(` 1. CI gate: ${arkCheckCommand(root)}`);
|
|
450
|
-
console.log(` 2. AI write gate: ${arkCommand(root, 'ark-mcp', '--root . --config ark.config.json')}`);
|
|
451
|
-
console.log(' (bind its validate_code tool to your agent\'s pre-write hook — see README)');
|
|
452
|
-
if (!hasCheckArchitectureScript(root)) {
|
|
453
|
-
console.log(' 3. Add the package.json alias if you want `run check:architecture`:');
|
|
454
|
-
console.log(` ${checkArchitectureScriptSnippet(root)}`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function buildConfigFromPolicyPack(packId, root) {
|
|
459
|
-
const pack = loadPolicyPackMeta(packId);
|
|
460
|
-
const factory = ARCHITECTURE_PRESETS[pack.preset];
|
|
461
|
-
if (!factory) {
|
|
462
|
-
throw new Error(
|
|
463
|
-
`Policy pack "${packId}" references unknown preset "${pack.preset}".`
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
const workspaces =
|
|
467
|
-
pack.preset === 'monorepo' || pack.preset === 'ui-surface'
|
|
468
|
-
? resolveIncludeRoots(root)
|
|
469
|
-
: [];
|
|
470
|
-
const config = factory(workspaces, root);
|
|
471
|
-
if (pack.layerDescriptions) {
|
|
472
|
-
for (const layer of config.layers) {
|
|
473
|
-
const enthusiast = pack.layerDescriptions[layer.name];
|
|
474
|
-
if (enthusiast) layer.description = enthusiast;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return { pack, config };
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function runListPolicyPacks(args) {
|
|
481
|
-
const ids = listPolicyPackIds();
|
|
482
|
-
if (args.json) {
|
|
483
|
-
const packs = ids.map((id) => {
|
|
484
|
-
const meta = loadPolicyPackMeta(id);
|
|
485
|
-
return {
|
|
486
|
-
id: meta.id,
|
|
487
|
-
preset: meta.preset,
|
|
488
|
-
variant: meta.variant,
|
|
489
|
-
label: meta.label,
|
|
490
|
-
summary: meta.summary,
|
|
491
|
-
phases: meta.phases,
|
|
492
|
-
};
|
|
493
|
-
});
|
|
494
|
-
console.log(JSON.stringify({ ok: true, packs }, null, 2));
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
console.log('Enthusiast policy packs (apply with --apply-policy-pack <id>):');
|
|
498
|
-
for (const id of ids) {
|
|
499
|
-
const meta = loadPolicyPackMeta(id);
|
|
500
|
-
console.log(` ${meta.id} — ${meta.label} (preset: ${meta.preset})`);
|
|
501
|
-
if (meta.summary) console.log(` ${meta.summary}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function runApplyPolicyPack(args) {
|
|
506
|
-
const configPath = path.isAbsolute(args.config)
|
|
507
|
-
? args.config
|
|
508
|
-
: path.join(args.root, args.config);
|
|
509
|
-
|
|
510
|
-
if (fs.existsSync(configPath) && !args.force) {
|
|
511
|
-
console.error(
|
|
512
|
-
`${configPath} already exists. Re-run with --force to overwrite, or use /ark-contract to evolve it.`
|
|
513
|
-
);
|
|
514
|
-
process.exitCode = 2;
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
try {
|
|
519
|
-
const { pack, config } = buildConfigFromPolicyPack(args.applyPolicyPack, args.root);
|
|
520
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
521
|
-
if (args.json) {
|
|
522
|
-
console.log(
|
|
523
|
-
JSON.stringify(
|
|
524
|
-
{
|
|
525
|
-
ok: true,
|
|
526
|
-
pack: pack.id,
|
|
527
|
-
preset: pack.preset,
|
|
528
|
-
configPath,
|
|
529
|
-
phases: pack.phases,
|
|
530
|
-
},
|
|
531
|
-
null,
|
|
532
|
-
2
|
|
533
|
-
)
|
|
534
|
-
);
|
|
535
|
-
} else {
|
|
536
|
-
console.log(`Wrote ${configPath} (${pack.label})`);
|
|
537
|
-
console.log(`Preset: ${pack.preset}. Phase 1: ${(pack.phases?.['1'] ?? []).join(', ')}`);
|
|
538
|
-
console.log(`Verify: ${arkCommand(args.root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
|
|
539
|
-
}
|
|
540
|
-
} catch (error) {
|
|
541
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
542
|
-
if (args.json) {
|
|
543
|
-
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
544
|
-
} else {
|
|
545
|
-
console.error(message);
|
|
546
|
-
}
|
|
547
|
-
process.exitCode = 2;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// A repo this size is not greenfield; a "starter" contract needs judgment `ark init` can't apply.
|
|
552
|
-
const BROWNFIELD_FILE_THRESHOLD = 150;
|
|
553
|
-
// Below this, the starter governs too thin a slice to be the real contract.
|
|
554
|
-
const THIN_COVERAGE_PERCENT = 50;
|
|
555
|
-
|
|
556
|
-
// `ark init` scaffolds a starter contract from conventional directory names (preset wildcards
|
|
557
|
-
// or convention detection). On a MATURE repo that pattern breaks down two ways: (1) most code
|
|
558
|
-
// lives in directories that don't match DDD names, so the starter governs a thin slice and a
|
|
559
|
-
// green check really means "unchecked"; (2) a broad glob like `src/**/domain/**` can swallow
|
|
560
|
-
// framework internals (e.g. src/kernel/domain) and fire domain-purity rules on them — 17
|
|
561
|
-
// violations that are a contract mismatch, not real debt. Both are exactly what the adoption
|
|
562
|
-
// flow (/ark-adopt) exists to resolve with structure-aware judgment. Detect the case and route
|
|
563
|
-
// there instead of leaving the user with a thin or false-red gate. Returns true if it warned.
|
|
564
|
-
function maybeWarnBrownfield(root, config) {
|
|
565
|
-
let files;
|
|
566
|
-
try {
|
|
567
|
-
files = collectGovernedFiles(root, config);
|
|
568
|
-
} catch {
|
|
569
|
-
return false;
|
|
570
|
-
}
|
|
571
|
-
if (files.length < BROWNFIELD_FILE_THRESHOLD) return false;
|
|
572
|
-
const cov = computeCoverage(root, config, files, config.rules ?? []);
|
|
573
|
-
if (cov.governed.percent >= THIN_COVERAGE_PERCENT) return false;
|
|
574
|
-
console.log('');
|
|
575
|
-
console.log(
|
|
576
|
-
`Heads up — this looks like an existing codebase (${files.length} source files), and this`
|
|
577
|
-
);
|
|
578
|
-
console.log(
|
|
579
|
-
`starter contract governs only ${cov.governed.percent}% of it (${cov.governed.classifiedFiles}/${files.length} files).`
|
|
580
|
-
);
|
|
581
|
-
console.log('`ark init` scaffolds from conventional directory names; on a mature repo that is');
|
|
582
|
-
console.log('usually a thin slice, and a broad domain glob can mis-flag framework internals as');
|
|
583
|
-
console.log('impure domain code (a contract mismatch, not real debt). For a contract aligned to');
|
|
584
|
-
console.log('your actual structure — governing more, with only genuine debt frozen — run adoption:');
|
|
585
|
-
console.log(` ${arkCommand(root, 'ark-check', '--recommend --write-plan')} # plan + ark-adoption-plan.json`);
|
|
586
|
-
console.log(' then run /ark-adopt in your agent # re-scope layers to reality, freeze real debt only');
|
|
587
|
-
console.log(`Inspect what is governed right now: ${arkCommand(root, 'ark-check', '--coverage')}`);
|
|
588
|
-
return true;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/** Propose include roots (workspaces + nested TS packages) — contract-adopt primitive. */
|
|
592
|
-
function runSuggestInclude(args) {
|
|
593
|
-
const root = args.root;
|
|
594
|
-
const workspaces = detectWorkspaces(root);
|
|
595
|
-
const tsPackages = detectTsPackageRoots(root);
|
|
596
|
-
const include = resolveIncludeRoots(root);
|
|
597
|
-
const payload = {
|
|
598
|
-
ok: true,
|
|
599
|
-
workspaces,
|
|
600
|
-
tsPackages,
|
|
601
|
-
suggestedInclude: include.length > 0 ? include : tsPackages.length > 0 ? tsPackages : ['src'],
|
|
602
|
-
note:
|
|
603
|
-
include.length === 0 && tsPackages.length === 0
|
|
604
|
-
? 'No TS packages or workspaces found — default suggestion is src/ (create it or pass include by hand).'
|
|
605
|
-
: 'Use these paths as ark.config.json "include". Prefer --adopt-contract --write to expand patterns too.',
|
|
606
|
-
};
|
|
607
|
-
if (args.json) {
|
|
608
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
console.log(color.bold('Suggested include roots'));
|
|
612
|
-
console.log(` workspaces: ${workspaces.join(', ') || '(none)'}`);
|
|
613
|
-
console.log(` tsPackages: ${tsPackages.join(', ') || '(none)'}`);
|
|
614
|
-
console.log(` suggestedInclude: ${payload.suggestedInclude.join(', ')}`);
|
|
615
|
-
console.log(color.dim(payload.note));
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Contract-adopt: expand include + presentation patterns from ungoverned proposals.
|
|
620
|
-
* Read-only unless --write. Does not weaken rules or baseline violations.
|
|
621
|
-
*/
|
|
622
|
-
function runAdoptContract(args) {
|
|
623
|
-
const root = args.root;
|
|
624
|
-
const configPath = path.isAbsolute(args.config)
|
|
625
|
-
? args.config
|
|
626
|
-
: path.join(root, args.config);
|
|
627
|
-
let config;
|
|
628
|
-
try {
|
|
629
|
-
config = fs.existsSync(configPath)
|
|
630
|
-
? readConfig(root, args.config)
|
|
631
|
-
: {
|
|
632
|
-
include: ['src'],
|
|
633
|
-
layers: ARCHITECTURE_PRESETS['ui-surface']([], root).layers,
|
|
634
|
-
rules: ARCHITECTURE_PRESETS['ui-surface']([], root).rules,
|
|
635
|
-
};
|
|
636
|
-
} catch (error) {
|
|
637
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
638
|
-
process.exitCode = 2;
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
const suggestedInclude = resolveIncludeRoots(root);
|
|
642
|
-
const tsPackages = detectTsPackageRoots(root);
|
|
643
|
-
const nextInclude = [
|
|
644
|
-
...new Set([
|
|
645
|
-
...(config.include || []),
|
|
646
|
-
...(suggestedInclude.length > 0 ? suggestedInclude : tsPackages),
|
|
647
|
-
]),
|
|
648
|
-
].filter(Boolean);
|
|
649
|
-
const files = collectGovernedFiles(root, { ...config, include: nextInclude.length ? nextInclude : config.include });
|
|
650
|
-
const cov = computeCoverage(root, { ...config, include: nextInclude.length ? nextInclude : config.include }, files, config.rules || []);
|
|
651
|
-
const uiPatterns = [
|
|
652
|
-
'**/components/**',
|
|
653
|
-
'**/hooks/**',
|
|
654
|
-
'**/lib/**',
|
|
655
|
-
'**/routes/**',
|
|
656
|
-
'**/app/**',
|
|
657
|
-
'**/pages/**',
|
|
658
|
-
];
|
|
659
|
-
const layers = (config.layers || []).map((layer) => {
|
|
660
|
-
if (layer.name !== 'PresentationAdapters') return layer;
|
|
661
|
-
const patterns = [...new Set([...(layer.patterns || []), ...uiPatterns])];
|
|
662
|
-
return { ...layer, patterns };
|
|
663
|
-
});
|
|
664
|
-
// If no PresentationAdapters layer, leave layers as-is (don't invent full profile).
|
|
665
|
-
const proposal = {
|
|
666
|
-
ok: true,
|
|
667
|
-
before: {
|
|
668
|
-
include: config.include || [],
|
|
669
|
-
governedPercent: null,
|
|
670
|
-
totalFiles: null,
|
|
671
|
-
},
|
|
672
|
-
after: {
|
|
673
|
-
include: nextInclude.length > 0 ? nextInclude : config.include,
|
|
674
|
-
presentationPatterns: uiPatterns,
|
|
675
|
-
totalFiles: cov.totalFiles,
|
|
676
|
-
governedPercent: cov.governed.percent,
|
|
677
|
-
unclassified: cov.unclassified.count,
|
|
678
|
-
},
|
|
679
|
-
wrote: false,
|
|
680
|
-
};
|
|
681
|
-
// Compute before coverage for honesty.
|
|
682
|
-
try {
|
|
683
|
-
const beforeFiles = collectGovernedFiles(root, config);
|
|
684
|
-
const beforeCov = computeCoverage(root, config, beforeFiles, config.rules || []);
|
|
685
|
-
proposal.before.totalFiles = beforeCov.totalFiles;
|
|
686
|
-
proposal.before.governedPercent = beforeCov.governed.percent;
|
|
687
|
-
} catch {
|
|
688
|
-
/* ignore */
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (args.write) {
|
|
692
|
-
const next = {
|
|
693
|
-
...config,
|
|
694
|
-
include: proposal.after.include,
|
|
695
|
-
layers,
|
|
696
|
-
};
|
|
697
|
-
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
698
|
-
proposal.wrote = true;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (args.json) {
|
|
702
|
-
console.log(JSON.stringify(proposal, null, 2));
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
console.log(color.bold('Contract adopt (coverage first)'));
|
|
706
|
-
console.log(
|
|
707
|
-
` before: include=[${(proposal.before.include || []).join(', ')}] governed=${proposal.before.governedPercent ?? '?'}% files=${proposal.before.totalFiles ?? '?'}`
|
|
708
|
-
);
|
|
709
|
-
console.log(
|
|
710
|
-
` after: include=[${(proposal.after.include || []).join(', ')}] governed=${proposal.after.governedPercent}% files=${proposal.after.totalFiles} unclassified=${proposal.after.unclassified}`
|
|
711
|
-
);
|
|
712
|
-
console.log(` presentation patterns += ${uiPatterns.join(', ')}`);
|
|
713
|
-
if (proposal.wrote) {
|
|
714
|
-
console.log(color.green(` wrote ${path.relative(root, configPath) || args.config}`));
|
|
715
|
-
console.log(color.dim(` Next: ${arkCommand(root, 'ark-check', '--coverage')} then --plan`));
|
|
716
|
-
} else {
|
|
717
|
-
console.log(color.dim(' Dry-run only. Re-run with --write to apply (does not weaken rules).'));
|
|
718
|
-
}
|
|
719
|
-
if ((proposal.after.totalFiles ?? 0) === 0) {
|
|
720
|
-
console.log(
|
|
721
|
-
color.yellow(
|
|
722
|
-
' Empty scope remains — no TS packages found. Point include at your package roots manually.'
|
|
723
|
-
)
|
|
724
|
-
);
|
|
725
|
-
process.exitCode = 1;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function runInit(args) {
|
|
730
|
-
const configPath = path.isAbsolute(args.config)
|
|
731
|
-
? args.config
|
|
732
|
-
: path.join(args.root, args.config);
|
|
733
|
-
|
|
734
|
-
if (fs.existsSync(configPath) && !args.force) {
|
|
735
|
-
console.error(`${configPath} already exists. Re-run with --force to overwrite it.`);
|
|
736
|
-
process.exitCode = 2;
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (args.preset) {
|
|
741
|
-
const factory = ARCHITECTURE_PRESETS[args.preset];
|
|
742
|
-
if (!factory) {
|
|
743
|
-
console.error(
|
|
744
|
-
`Unknown preset "${args.preset}". Valid presets: ${Object.keys(ARCHITECTURE_PRESETS).join(', ')}.`
|
|
745
|
-
);
|
|
746
|
-
process.exitCode = 2;
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
const finalConfig = factory(
|
|
750
|
-
args.preset === 'monorepo' || args.preset === 'ui-surface'
|
|
751
|
-
? resolveIncludeRoots(args.root)
|
|
752
|
-
: detectWorkspaces(args.root),
|
|
753
|
-
args.root
|
|
754
|
-
);
|
|
755
|
-
fs.writeFileSync(configPath, `${JSON.stringify(finalConfig, null, 2)}\n`);
|
|
756
|
-
console.log(`Wrote ${configPath} (${args.preset} preset)`);
|
|
757
|
-
if (finalConfig.frameworkOverlay) {
|
|
758
|
-
console.log(
|
|
759
|
-
`Framework layout overlay applied: ${finalConfig.frameworkOverlay} (filename conventions merged into layer globs).`
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
console.log('');
|
|
763
|
-
console.log('Layers (every layer optional, so the strict check passes before the directories exist):');
|
|
764
|
-
for (const layer of finalConfig.layers) {
|
|
765
|
-
console.log(` ${layer.name}: ${layer.patterns.join(', ')}`);
|
|
766
|
-
}
|
|
767
|
-
if (args.preset === 'monorepo') {
|
|
768
|
-
console.log('');
|
|
769
|
-
console.log(`include: ${finalConfig.include.join(', ')} — patterns match by directory name in any`);
|
|
770
|
-
console.log(`package; adjust to your naming, then verify: ${arkCommand(args.root, 'ark-check', '--coverage')}`);
|
|
771
|
-
}
|
|
772
|
-
maybeWarnBrownfield(args.root, finalConfig);
|
|
773
|
-
printInitNextSteps(args.root);
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const { srcDir, config } = detectConfig(args.root);
|
|
778
|
-
const greenfield = config.layers.length === 0;
|
|
779
|
-
// When no conventional src/ layout is found, a `workspaces` declaration means this is a
|
|
780
|
-
// monorepo — the src/** 11-layer starter would match nothing there, so use the
|
|
781
|
-
// cross-package monorepo profile anchored at the real workspace roots instead.
|
|
782
|
-
const includeRoots = greenfield ? resolveIncludeRoots(args.root) : [];
|
|
783
|
-
const tsPackages = greenfield ? detectTsPackageRoots(args.root) : [];
|
|
784
|
-
// Prefer monorepo/ui when nested TS packages exist without a conventional src layout.
|
|
785
|
-
const mode = !greenfield
|
|
786
|
-
? 'detected'
|
|
787
|
-
: includeRoots.length > 0 || tsPackages.length > 0
|
|
788
|
-
? 'monorepo'
|
|
789
|
-
: 'greenfield';
|
|
790
|
-
// Greenfield: anchor the starter profile at src/ (the convention a fresh project will
|
|
791
|
-
// scaffold under) even when src/ doesn't exist yet — the layers are optional, so the
|
|
792
|
-
// check passes today and governance switches on the moment src/domain/ etc. appear.
|
|
793
|
-
// Detected configs also get framework overlays so Nest/Next flat files are classified.
|
|
794
|
-
const finalConfig =
|
|
795
|
-
mode === 'detected'
|
|
796
|
-
? applyFrameworkLayoutOverlays(config, args.root)
|
|
797
|
-
: mode === 'monorepo'
|
|
798
|
-
? ARCHITECTURE_PRESETS.monorepo(
|
|
799
|
-
includeRoots.length > 0 ? includeRoots : tsPackages,
|
|
800
|
-
args.root
|
|
801
|
-
)
|
|
802
|
-
: createElevenLayerConfig({
|
|
803
|
-
rootDir: srcDir === '.' ? 'src' : srcDir,
|
|
804
|
-
root: args.root,
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
fs.writeFileSync(configPath, `${JSON.stringify(finalConfig, null, 2)}\n`);
|
|
808
|
-
|
|
809
|
-
console.log(`Wrote ${configPath}`);
|
|
810
|
-
console.log('');
|
|
811
|
-
if (mode === 'monorepo') {
|
|
812
|
-
const roots = finalConfig.include?.join(', ') || '(none)';
|
|
813
|
-
console.log(`Multi-package / TS package surface detected (include: ${roots}). Generated a`);
|
|
814
|
-
console.log('cross-package profile matching domain/application/presentation/persistence dirs');
|
|
815
|
-
console.log('in any package. Every layer is optional, so the strict check passes now and each');
|
|
816
|
-
console.log('switches on as matching directories gain files. Adjust patterns to your naming:');
|
|
817
|
-
for (const layer of finalConfig.layers) {
|
|
818
|
-
console.log(` ${layer.name}: ${layer.patterns.join(', ')}`);
|
|
819
|
-
}
|
|
820
|
-
console.log('');
|
|
821
|
-
console.log(`Verify what each layer actually governs: ${arkCommand(args.root, 'ark-check', '--coverage')}`);
|
|
822
|
-
} else if (mode === 'greenfield') {
|
|
823
|
-
console.log('No conventional layer directories found — generated the full 11-layer starter');
|
|
824
|
-
console.log('profile instead. Every layer is marked optional, so the strict check passes now');
|
|
825
|
-
console.log('and each layer starts being enforced as soon as its directory gains source files:');
|
|
826
|
-
for (const layer of finalConfig.layers) {
|
|
827
|
-
console.log(` ${layer.name}: ${layer.patterns.join(', ')}`);
|
|
828
|
-
}
|
|
829
|
-
// The starter profile only governs src/. Existing source elsewhere would make the
|
|
830
|
-
// gate silently green, so surface it instead of pretending the project is covered.
|
|
831
|
-
const outside = walk(args.root, [], { root: args.root })
|
|
832
|
-
.map((file) => normalize(path.relative(args.root, file)))
|
|
833
|
-
.filter((rel) => !rel.startsWith('src/') && !rel.split('/').some((s) => s.startsWith('.')));
|
|
834
|
-
if (outside.length > 0) {
|
|
835
|
-
console.log('');
|
|
836
|
-
console.log(`WARNING: ${outside.length} source file(s) live outside src/ and are NOT governed`);
|
|
837
|
-
console.log(`by this config (e.g. ${outside.slice(0, 3).join(', ')}).`);
|
|
838
|
-
console.log('Move them under src/, or edit the "include" and layer patterns to match your layout.');
|
|
839
|
-
}
|
|
840
|
-
} else {
|
|
841
|
-
console.log('Detected layers:');
|
|
842
|
-
for (const layer of finalConfig.layers) {
|
|
843
|
-
console.log(` ${layer.name}: ${layer.patterns.join(', ')}`);
|
|
844
|
-
}
|
|
845
|
-
const detected = new Set(finalConfig.layers.map((layer) => layer.name));
|
|
846
|
-
const suggested = DEFAULT_INTENT_PREFIXES.filter((entry) => !detected.has(entry.layer));
|
|
847
|
-
if (suggested.length > 0) {
|
|
848
|
-
console.log('');
|
|
849
|
-
console.log('Suggested layers from the 11-layer profile (not detected — conventional');
|
|
850
|
-
console.log('directories shown; create one and re-run --init, or add the layer by hand):');
|
|
851
|
-
for (const entry of suggested) {
|
|
852
|
-
const dirs = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? [])
|
|
853
|
-
.map((directory) => `${srcDir}/${directory}`)
|
|
854
|
-
.join(', ');
|
|
855
|
-
console.log(` ${entry.layer}: ${dirs}`);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
const proposals = proposeForUncovered(args.root, srcDir, finalConfig.layers);
|
|
859
|
-
if (proposals.length > 0) {
|
|
860
|
-
const recognized = proposals.filter((p) => !p.unrecognized);
|
|
861
|
-
const unrecognized = proposals.filter((p) => p.unrecognized);
|
|
862
|
-
console.log('');
|
|
863
|
-
console.log('Ungoverned directories — Ark enforces NOTHING here until they are classified.');
|
|
864
|
-
console.log('A green check ignores this code; it is not "clean", it is unchecked.');
|
|
865
|
-
if (recognized.length > 0) {
|
|
866
|
-
console.log('');
|
|
867
|
-
console.log('Proposed layer for each (from the 11-layer profile + presets — apply via /ark-contract):');
|
|
868
|
-
for (const p of recognized) {
|
|
869
|
-
const alt = p.alternatives?.length ? ` (or ${p.alternatives.join(' / ')} — confirm)` : '';
|
|
870
|
-
console.log(` ${p.dir}/ → ${p.layer}${alt}`);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (unrecognized.length > 0) {
|
|
874
|
-
console.log('');
|
|
875
|
-
console.log(`Not recognized — you decide the layer: ${unrecognized.map((p) => p.dir).join(', ')}`);
|
|
876
|
-
}
|
|
877
|
-
const fit = detectBestFitModel(
|
|
878
|
-
[
|
|
879
|
-
...finalConfig.layers.flatMap((l) => (l.patterns ?? []).map((p) => dirSegmentsFromGlob(p).pop())),
|
|
880
|
-
...proposals.map((p) => p.dir.split('/').pop()),
|
|
881
|
-
].filter(Boolean)
|
|
882
|
-
);
|
|
883
|
-
if (fit) {
|
|
884
|
-
console.log('');
|
|
885
|
-
console.log(`Closest starter model: ${fit.name} — \`ark init --preset ${fit.name} --force\` to start from its rule set.`);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
maybeWarnBrownfield(args.root, finalConfig);
|
|
890
|
-
printInitNextSteps(args.root);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function readManifest(root, manifestPath) {
|
|
894
|
-
if (!manifestPath) return undefined;
|
|
895
|
-
const fullPath = path.isAbsolute(manifestPath)
|
|
896
|
-
? manifestPath
|
|
897
|
-
: path.join(root, manifestPath);
|
|
898
|
-
if (!fs.existsSync(fullPath)) {
|
|
899
|
-
throw new Error(`Manifest not found: ${fullPath}`);
|
|
34
|
+
if (!BOOLEAN_FLAGS.has(flag)) return null;
|
|
35
|
+
if (flag === '--resident') args.resident = true;
|
|
36
|
+
else if (flag === '--doctor') args.doctor = true;
|
|
37
|
+
else if (flag === '--json') args.json = true;
|
|
900
38
|
}
|
|
901
|
-
return
|
|
39
|
+
return args.resident && args.doctor && args.json ? args : null;
|
|
902
40
|
}
|
|
903
41
|
|
|
904
|
-
const useColor = process.stderr.isTTY && !process.env.NO_COLOR;
|
|
905
|
-
const color = {
|
|
906
|
-
red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s),
|
|
907
|
-
yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
|
|
908
|
-
green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
|
|
909
|
-
dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
|
|
910
|
-
bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
|
|
911
|
-
};
|
|
912
|
-
|
|
913
42
|
async function main() {
|
|
914
|
-
const args =
|
|
915
|
-
if (args
|
|
916
|
-
|
|
917
|
-
process.exit(0);
|
|
918
|
-
}
|
|
919
|
-
if (args.help) {
|
|
920
|
-
console.log(usage());
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
if (args.init) {
|
|
924
|
-
runInit(args);
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (args.installAgentGates) {
|
|
928
|
-
runInstallAgentGates(args);
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
if (args.printConfig) {
|
|
932
|
-
if (args.printConfig !== 'eleven-layer') {
|
|
933
|
-
console.error(`Unknown config profile: ${args.printConfig}`);
|
|
934
|
-
process.exitCode = 2;
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
console.log(JSON.stringify(createElevenLayerConfig(), null, 2));
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
if (args.listPolicyPacks) {
|
|
942
|
-
runListPolicyPacks(args);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
if (args.applyPolicyPack) {
|
|
947
|
-
runApplyPolicyPack(args);
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
if (args.suggestInclude) {
|
|
952
|
-
runSuggestInclude(args);
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (args.adoptContract) {
|
|
957
|
-
runAdoptContract(args);
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (args.recommend) {
|
|
962
|
-
try {
|
|
963
|
-
const recommendation = buildArchitectureRecommendation(args.root);
|
|
964
|
-
let planWritten;
|
|
965
|
-
if (args.writePlan) {
|
|
966
|
-
const result = writeAdoptionPlan(args.root, recommendation);
|
|
967
|
-
planWritten = result.path;
|
|
968
|
-
}
|
|
969
|
-
if (args.json) {
|
|
970
|
-
console.log(
|
|
971
|
-
JSON.stringify(
|
|
972
|
-
{
|
|
973
|
-
...recommendation,
|
|
974
|
-
...(planWritten
|
|
975
|
-
? { adoptionPlanPath: path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME }
|
|
976
|
-
: {}),
|
|
977
|
-
},
|
|
978
|
-
null,
|
|
979
|
-
2
|
|
980
|
-
)
|
|
981
|
-
);
|
|
982
|
-
} else {
|
|
983
|
-
console.log(formatArchitectureRecommendationHuman(recommendation));
|
|
984
|
-
if (planWritten) {
|
|
985
|
-
console.log('');
|
|
986
|
-
console.log(`Wrote ${path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME}`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
} catch (error) {
|
|
990
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
991
|
-
if (args.json) {
|
|
992
|
-
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
993
|
-
} else {
|
|
994
|
-
console.error(`ark-check --recommend failed: ${message}`);
|
|
995
|
-
}
|
|
996
|
-
process.exitCode = 2;
|
|
997
|
-
}
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (args.requireGates || args.requireWriteHook) {
|
|
1002
|
-
let writeRequest = null;
|
|
1003
|
-
if (args.requireWriteHook) {
|
|
1004
|
-
writeRequest = validateHardWriteRequest({
|
|
1005
|
-
root: args.root,
|
|
1006
|
-
host: args.requireWriteHook,
|
|
1007
|
-
tools: [args.requireWriteHook],
|
|
1008
|
-
force: true,
|
|
1009
|
-
});
|
|
1010
|
-
if (!writeRequest.ok) {
|
|
1011
|
-
const payload = {
|
|
1012
|
-
ok: false,
|
|
1013
|
-
error: 'unsupported-enforcement-profile',
|
|
1014
|
-
message: writeRequest.error,
|
|
1015
|
-
};
|
|
1016
|
-
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
1017
|
-
else console.error(writeRequest.error);
|
|
1018
|
-
process.exitCode = 2;
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
const missing = args.requireGates ? missingGates(args.root) : [];
|
|
1024
|
-
if (
|
|
1025
|
-
writeRequest?.host &&
|
|
1026
|
-
!detectWritePathCapabilities(args.root, writeRequest.host).capabilities['hard-write']
|
|
1027
|
-
) {
|
|
1028
|
-
missing.push(`${writeRequest.host} hard-write hook`);
|
|
1029
|
-
}
|
|
1030
|
-
if (missing.length > 0) {
|
|
1031
|
-
const payload = {
|
|
1032
|
-
ok: false,
|
|
1033
|
-
error: 'missing-gates',
|
|
1034
|
-
missing,
|
|
1035
|
-
...(writeRequest?.host ? { writeHost: writeRequest.host } : {}),
|
|
1036
|
-
};
|
|
1037
|
-
if (args.json) {
|
|
1038
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
1039
|
-
} else {
|
|
1040
|
-
console.error('Ark gates are not installed. Missing:');
|
|
1041
|
-
for (const relativePath of missing) {
|
|
1042
|
-
console.error(` - ${relativePath}`);
|
|
1043
|
-
}
|
|
1044
|
-
const installArgs = writeRequest?.host
|
|
1045
|
-
? `--install-agent-gates --tools ${writeRequest.host} --require-write-hook ${writeRequest.host}`
|
|
1046
|
-
: '--install-agent-gates';
|
|
1047
|
-
console.error(
|
|
1048
|
-
`\nRun \`${arkCommand(args.root, 'ark', 'init')}\` (or \`${arkCommand(args.root, 'ark-check', installArgs)}\`) to configure enforcement.`
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
process.exitCode = 1;
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
// Gates present. This is a precondition, not a standalone report: stay quiet
|
|
1055
|
-
// in --json mode so the architecture check below owns the single JSON output.
|
|
1056
|
-
// When --require-gates is the only intent (no config/architecture run needed),
|
|
1057
|
-
// callers still get a clear signal from the exit code and the human-mode line.
|
|
1058
|
-
if (!args.json) {
|
|
1059
|
-
if (args.requireGates) {
|
|
1060
|
-
const compactHost = compactRouterHost(args.root);
|
|
1061
|
-
console.log(
|
|
1062
|
-
'Ark gates present (merge profile): ' +
|
|
1063
|
-
(compactHost
|
|
1064
|
-
? `AGENTS.md, compact host registration (${compactHost})`
|
|
1065
|
-
: REQUIRED_GATE_FILES.join(', '))
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
if (writeRequest?.host) {
|
|
1069
|
-
console.log(`Ark hard-write hook present for ${writeRequest.host}.`);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
const root = args.root;
|
|
1075
|
-
const config = readConfig(root, args.config);
|
|
1076
|
-
const policyDelta = analyzePolicyTransition({
|
|
1077
|
-
root,
|
|
1078
|
-
configPath: args.config,
|
|
1079
|
-
candidateConfig: config,
|
|
1080
|
-
strictMerge: args.strictMerge,
|
|
1081
|
-
basePath: args.policyBase,
|
|
1082
|
-
baseRef: args.policyBaseRef,
|
|
1083
|
-
acknowledgementPath: args.policyAck,
|
|
1084
|
-
});
|
|
1085
|
-
const manifest = readManifest(root, args.manifest);
|
|
1086
|
-
const rules = manifest?.architecture?.rules ?? config.rules;
|
|
1087
|
-
const files = collectGovernedFiles(root, config);
|
|
1088
|
-
|
|
1089
|
-
// --coverage is a pure glob/report view (no TypeScript resolver), so serve it BEFORE the
|
|
1090
|
-
// TS import: the report must work — and exit 0 — even when typescript isn't installed.
|
|
1091
|
-
if (args.coverage) {
|
|
1092
|
-
runCoverage(root, config, files, rules, args.json);
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// Resolve TypeScript from the project first, then Ark's own install, then bare import.
|
|
1097
|
-
// --plan can still run honestly (coverage + empty violations) when TS is missing.
|
|
1098
|
-
// Early TypeScript 7 native builds may load but lack a JS `sys` host — we fall back.
|
|
1099
|
-
const loaded = await loadTypeScript(root);
|
|
1100
|
-
if (!loaded?.ts) {
|
|
1101
|
-
if (args.plan) {
|
|
1102
|
-
const cov = computeCoverage(root, config, files, rules);
|
|
1103
|
-
if (!args.json) {
|
|
1104
|
-
console.log(
|
|
1105
|
-
color.yellow(
|
|
1106
|
-
`TypeScript not found — plan shows coverage honesty only (no import graph). Install with: ${installDevHint(root, 'typescript')} (supported: 5.x–7.x; see docs/typescript-support.md)`
|
|
1107
|
-
)
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
runPlan(root, [], args.json, cov.governed.percent, cov.governed.totalFiles, {
|
|
1111
|
-
config,
|
|
1112
|
-
files,
|
|
1113
|
-
coverage: cov,
|
|
1114
|
-
});
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
console.error(
|
|
1118
|
-
`ark-check requires a JS-API TypeScript (5.x–7.x with ts.sys). Install with: ${installDevHint(root, 'typescript')} — see docs/typescript-support.md`
|
|
1119
|
-
);
|
|
1120
|
-
process.exitCode = 2;
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
const { ts } = loaded;
|
|
1124
|
-
if (loaded.fallbackReason && !args.json) {
|
|
1125
|
-
console.log(color.yellow(loaded.fallbackReason));
|
|
1126
|
-
}
|
|
1127
|
-
if (process.env.ARK_DEBUG_TS === '1' && !args.json) {
|
|
1128
|
-
console.log(
|
|
1129
|
-
color.dim(
|
|
1130
|
-
`[ark-check] TypeScript ${loaded.version ?? '?'} via ${loaded.source}` +
|
|
1131
|
-
(loaded.fallbackReason ? ' (fallback)' : '')
|
|
1132
|
-
)
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
const { violations, warnings, safety } = runArchitectureScan({
|
|
1137
|
-
root,
|
|
1138
|
-
config,
|
|
1139
|
-
manifest,
|
|
1140
|
-
rules,
|
|
1141
|
-
files,
|
|
1142
|
-
ts,
|
|
1143
|
-
args,
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
if (args.doctor) {
|
|
1147
|
-
runDoctor(root, config, files, rules, violations, args.json, {
|
|
1148
|
-
configPath: path.isAbsolute(args.config) ? args.config : path.join(root, args.config),
|
|
1149
|
-
configMissing: !fs.existsSync(path.isAbsolute(args.config) ? args.config : path.join(root, args.config)),
|
|
1150
|
-
safety,
|
|
1151
|
-
ts,
|
|
1152
|
-
});
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
if (args.ratchetCores) {
|
|
1157
|
-
runRatchetCores(root, config, files, rules, violations, args, { displayPathFromRoot });
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
if (args.updateBaseline) {
|
|
1162
|
-
const summary = summarizeViolations(violations);
|
|
1163
|
-
// Bloquear y avisar: a lopsided freeze buries a likely contract bug as "debt". Refuse it
|
|
1164
|
-
// (unless --force), diagnose, and point at the contract fix instead of the baseline.
|
|
1165
|
-
if (summary.concentrated && !args.force) {
|
|
1166
|
-
console.error(
|
|
1167
|
-
`Refusing to freeze ${summary.total} violations: ${Math.round(summary.dominantShare * 100)}% are a single edge (${summary.dominant}).`
|
|
1168
|
-
);
|
|
1169
|
-
printViolationBreakdown(summary, { toStderr: true });
|
|
1170
|
-
console.error('');
|
|
1171
|
-
console.error('Freezing this would bury a likely CONTRACT bug as "debt". Fix the contract');
|
|
1172
|
-
console.error('first (/ark-contract), then re-run. To freeze anyway: --update-baseline --force.');
|
|
1173
|
-
process.exitCode = 2;
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
const baselineName = args.baseline || '.ark-baseline.json';
|
|
1177
|
-
const fullBaselinePath = path.isAbsolute(baselineName)
|
|
1178
|
-
? baselineName
|
|
1179
|
-
: path.join(root, baselineName);
|
|
1180
|
-
// Zero debt: do not leave an empty baseline file (unclear policy — "is ratchet on?").
|
|
1181
|
-
// Delete any existing empty/orphan baseline so doctor/CI stay honest.
|
|
1182
|
-
if (violations.length === 0) {
|
|
1183
|
-
if (fs.existsSync(fullBaselinePath)) {
|
|
1184
|
-
fs.unlinkSync(fullBaselinePath);
|
|
1185
|
-
console.log(
|
|
1186
|
-
`No violations to freeze — removed empty baseline ${fullBaselinePath} (zero debt; no ratchet file needed).`
|
|
1187
|
-
);
|
|
1188
|
-
} else {
|
|
1189
|
-
console.log('No violations to freeze — baseline not written (zero debt).');
|
|
1190
|
-
}
|
|
1191
|
-
console.log('Gate with: ark-check --root . --config ark.config.json --strict-config');
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
const { fullPath, count } = writeBaseline(root, baselineName, violations);
|
|
1195
|
-
console.log(`Wrote ${fullPath} with ${count} frozen violation key(s).`);
|
|
1196
|
-
// Keep existing package.json scripts + CI workflows on the ratchet without a
|
|
1197
|
-
// full --force reinstall (field log: baseline after start left CI without --baseline).
|
|
1198
|
-
const baselineRel = path.isAbsolute(baselineName)
|
|
1199
|
-
? path.relative(root, baselineName).split(path.sep).join('/')
|
|
1200
|
-
: String(baselineName).replace(/^\.\/+/, '');
|
|
1201
|
-
const sync = syncBaselineIntoCheckSurfaces(root, {
|
|
1202
|
-
baselineRel: baselineRel || '.ark-baseline.json',
|
|
1203
|
-
});
|
|
1204
|
-
if (sync.changed.length > 0) {
|
|
1205
|
-
console.log(
|
|
1206
|
-
`Synced --baseline into: ${sync.changed.map((c) => c.file).join(', ')}`
|
|
1207
|
-
);
|
|
1208
|
-
} else {
|
|
1209
|
-
console.log(
|
|
1210
|
-
'No existing check scripts/workflows needed a --baseline patch (add check:architecture or re-run --install-agent-gates).'
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
console.log('Commit it and gate CI with: ark-check --baseline (only NEW violations fail).');
|
|
1214
|
-
if (summary.total > 0) printViolationBreakdown(summary);
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
let suppressed = [];
|
|
1219
|
-
let activeViolations = violations;
|
|
1220
|
-
let staleBaselineKeys = 0;
|
|
1221
|
-
if (args.baseline) {
|
|
1222
|
-
const baseline = readBaseline(root, args.baseline);
|
|
1223
|
-
if (baseline.exists) {
|
|
1224
|
-
const occurrenceKeys = baselineOccurrenceKeys(violations);
|
|
1225
|
-
suppressed = violations.filter((_, index) => baseline.keys.has(occurrenceKeys[index]));
|
|
1226
|
-
activeViolations = violations.filter((_, index) => !baseline.keys.has(occurrenceKeys[index]));
|
|
1227
|
-
const currentKeys = new Set(occurrenceKeys);
|
|
1228
|
-
staleBaselineKeys = [...baseline.keys].filter((key) => !currentKeys.has(key)).length;
|
|
1229
|
-
} else {
|
|
1230
|
-
warnings.push(
|
|
1231
|
-
configWarning(
|
|
1232
|
-
'BASELINE_NOT_FOUND',
|
|
1233
|
-
`Baseline file not found: ${baseline.fullPath}. Generate it with: ark-check --update-baseline`
|
|
1234
|
-
)
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// Soft/advisory warnings (failsStrict === false) never fail --strict-config.
|
|
1240
|
-
const strictWarnings = warnings.filter((w) => w.failsStrict !== false);
|
|
1241
|
-
const ok =
|
|
1242
|
-
activeViolations.length === 0 &&
|
|
1243
|
-
(!args.strictConfig || strictWarnings.length === 0) &&
|
|
1244
|
-
(policyDelta?.valid ?? true);
|
|
1245
|
-
|
|
1246
|
-
if (args.plan) {
|
|
1247
|
-
const cov = computeCoverage(root, config, files, rules);
|
|
1248
|
-
runPlan(root, activeViolations, args.json, cov.governed.percent, cov.governed.totalFiles, {
|
|
1249
|
-
config,
|
|
1250
|
-
files,
|
|
1251
|
-
coverage: cov,
|
|
1252
|
-
});
|
|
43
|
+
const args = residentArgs(process.argv);
|
|
44
|
+
if (!args) {
|
|
45
|
+
await import('./ark-check-runtime.mjs');
|
|
1253
46
|
return;
|
|
1254
47
|
}
|
|
48
|
+
if (await tryResidentDoctor(args)) return;
|
|
1255
49
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (args.report) {
|
|
1262
|
-
const exampleByLayer = new Map();
|
|
1263
|
-
const fileCountByLayer = new Map();
|
|
1264
|
-
for (const file of files) {
|
|
1265
|
-
const layer = layerForFile(root, file, config.layers);
|
|
1266
|
-
if (!layer) continue;
|
|
1267
|
-
fileCountByLayer.set(layer, (fileCountByLayer.get(layer) || 0) + 1);
|
|
1268
|
-
if (!exampleByLayer.has(layer)) {
|
|
1269
|
-
exampleByLayer.set(layer, normalize(path.relative(root, file)));
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
const coverage = computeCoverage(root, config, files, rules);
|
|
1273
|
-
const enforcementForReport = detectEnforcement(root);
|
|
1274
|
-
const fitness = computeReportFitness({
|
|
1275
|
-
coverage,
|
|
1276
|
-
violations: activeViolations,
|
|
1277
|
-
ok,
|
|
1278
|
-
enforcement: enforcementForReport,
|
|
1279
|
-
config,
|
|
1280
|
-
});
|
|
1281
|
-
const currentSnapshot = buildReportSnapshot({
|
|
1282
|
-
root,
|
|
1283
|
-
config,
|
|
1284
|
-
coverage,
|
|
1285
|
-
violations: activeViolations,
|
|
1286
|
-
ok,
|
|
1287
|
-
suppressed: suppressed.length,
|
|
1288
|
-
version: arkPackageVersion(),
|
|
1289
|
-
fileCountByLayer,
|
|
1290
|
-
enforcement: enforcementForReport,
|
|
1291
|
-
score: fitness.score,
|
|
1292
|
-
mode: fitness.mode,
|
|
1293
|
-
});
|
|
1294
|
-
// Origin is read before archive so the HTML can show "just created" vs deltas.
|
|
1295
|
-
const existingOrigin = args.resetOrigin
|
|
1296
|
-
? null
|
|
1297
|
-
: readJsonSafe(path.join(reportsDir(root), 'origin.json'));
|
|
1298
|
-
const { adoption: adoptionForReport, designDepth } = buildReportDepthPayload(
|
|
1299
|
-
root,
|
|
1300
|
-
config,
|
|
1301
|
-
files,
|
|
1302
|
-
coverage,
|
|
1303
|
-
activeViolations
|
|
1304
|
-
);
|
|
1305
|
-
const reportPayload = {
|
|
1306
|
-
root,
|
|
1307
|
-
config,
|
|
1308
|
-
exampleByLayer,
|
|
1309
|
-
fileCountByLayer,
|
|
1310
|
-
coverage,
|
|
1311
|
-
violations: activeViolations,
|
|
1312
|
-
ok,
|
|
1313
|
-
suppressed: suppressed.length,
|
|
1314
|
-
version: arkPackageVersion(),
|
|
1315
|
-
configPath: args.config,
|
|
1316
|
-
generatedAt: new Date().toISOString().slice(0, 10),
|
|
1317
|
-
skillGaps,
|
|
1318
|
-
originSnapshot: existingOrigin,
|
|
1319
|
-
currentSnapshot,
|
|
1320
|
-
originJustCreated: !existingOrigin,
|
|
1321
|
-
adoption: adoptionForReport,
|
|
1322
|
-
designDepth,
|
|
1323
|
-
advisories: computeDoctorAdvisories(root, config, coverage, rules, files, ts),
|
|
1324
|
-
};
|
|
1325
|
-
const html = args.beginner
|
|
1326
|
-
? renderBeginnerHtmlReport(reportPayload)
|
|
1327
|
-
: renderHtmlReport(reportPayload);
|
|
1328
|
-
const reportPath = path.isAbsolute(args.report) ? args.report : path.join(root, args.report);
|
|
1329
|
-
fs.writeFileSync(reportPath, html);
|
|
1330
|
-
|
|
1331
|
-
const archive = archiveReportSnapshots(root, {
|
|
1332
|
-
html,
|
|
1333
|
-
snapshot: currentSnapshot,
|
|
1334
|
-
resetOrigin: Boolean(args.resetOrigin),
|
|
1335
|
-
noArchive: Boolean(args.noArchive),
|
|
1336
|
-
});
|
|
1337
|
-
if (!args.json) {
|
|
1338
|
-
console.log(`${color.green('✎')} Wrote HTML report: ${displayPathFromRoot(root, reportPath)}`);
|
|
1339
|
-
if (archive.createdOrigin) {
|
|
1340
|
-
console.log(
|
|
1341
|
-
`${color.green('✎')} Origin snapshot saved (first report): ${displayPathFromRoot(root, archive.originJson)}`
|
|
1342
|
-
);
|
|
1343
|
-
console.log(
|
|
1344
|
-
color.dim(' Future reports will show evolution vs this starting point (.ark/reports/).')
|
|
1345
|
-
);
|
|
1346
|
-
} else {
|
|
1347
|
-
console.log(
|
|
1348
|
-
color.dim(
|
|
1349
|
-
` Snapshots: .ark/reports/latest.* (origin frozen${args.resetOrigin ? ' — reset this run' : ''})`
|
|
1350
|
-
)
|
|
1351
|
-
);
|
|
1352
|
-
}
|
|
1353
|
-
// Nudge .gitignore for loose report files (origin/latest already under .ark/).
|
|
1354
|
-
const gitignore = path.join(root, '.gitignore');
|
|
1355
|
-
const base = path.basename(reportPath);
|
|
1356
|
-
if (!path.isAbsolute(args.report) && fs.existsSync(gitignore)) {
|
|
1357
|
-
const ignored = fs
|
|
1358
|
-
.readFileSync(gitignore, 'utf8')
|
|
1359
|
-
.split('\n')
|
|
1360
|
-
.some(
|
|
1361
|
-
(line) =>
|
|
1362
|
-
line.trim() === base ||
|
|
1363
|
-
line.trim() === `/${base}` ||
|
|
1364
|
-
line.trim() === args.report ||
|
|
1365
|
-
line.trim() === '.ark/' ||
|
|
1366
|
-
line.trim() === '.ark'
|
|
1367
|
-
);
|
|
1368
|
-
if (!ignored) {
|
|
1369
|
-
console.log(
|
|
1370
|
-
color.dim(` (generated artifact — prefer .ark/reports/; add "${base}" or ".ark/" to .gitignore)`)
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// Best-effort: open the report in the default browser. If it opens, fine;
|
|
1377
|
-
// if not (headless, no GUI, spawn error), do nothing — never fail the check.
|
|
1378
|
-
// Skipped in CI / Vitest / ARK_NO_OPEN_REPORT; --open / --no-open override.
|
|
1379
|
-
if (
|
|
1380
|
-
shouldOpenHtmlReport({
|
|
1381
|
-
force: Boolean(args.openReport),
|
|
1382
|
-
noOpen: Boolean(args.noOpenReport) || Boolean(args.json),
|
|
1383
|
-
})
|
|
1384
|
-
) {
|
|
1385
|
-
openHtmlInBrowser(reportPath);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
if (args.json) {
|
|
1390
|
-
const adapterResult = createAdapterResult({
|
|
1391
|
-
valid: ok,
|
|
1392
|
-
violations: activeViolations.map(enrichViolationWithFixClass),
|
|
1393
|
-
warnings,
|
|
1394
|
-
});
|
|
1395
|
-
console.log(JSON.stringify({
|
|
1396
|
-
...adapterResult,
|
|
1397
|
-
ok,
|
|
1398
|
-
violations: activeViolations.map(enrichViolationWithFixClass),
|
|
1399
|
-
suppressedViolations: suppressed.length,
|
|
1400
|
-
staleBaselineKeys,
|
|
1401
|
-
warnings,
|
|
1402
|
-
...(activeViolations.length > 0 ? { summary: summarizeViolations(activeViolations) } : {}),
|
|
1403
|
-
...(skillGaps.length > 0 ? { skillGaps } : {}),
|
|
1404
|
-
...(codexHomeGap
|
|
1405
|
-
? {
|
|
1406
|
-
codexHomeGap: {
|
|
1407
|
-
...codexHomeGap,
|
|
1408
|
-
deferred: !codexSessionActive,
|
|
1409
|
-
},
|
|
1410
|
-
}
|
|
1411
|
-
: {}),
|
|
1412
|
-
...(codexRepoSkillGap ? { codexRepoSkillGap } : {}),
|
|
1413
|
-
...(policyDelta ? { policyDelta } : {}),
|
|
1414
|
-
}, null, 2));
|
|
1415
|
-
} else {
|
|
1416
|
-
for (const warning of warnings) {
|
|
1417
|
-
console.error(`${color.yellow('warning')} ${warning.ruleId} ${warning.message}`);
|
|
1418
|
-
}
|
|
1419
|
-
for (const violation of activeViolations) {
|
|
1420
|
-
printViolation(violation);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
const baselineNote =
|
|
1424
|
-
suppressed.length > 0 ? ` (${suppressed.length} suppressed by baseline)` : '';
|
|
1425
|
-
if (staleBaselineKeys > 0) {
|
|
1426
|
-
console.error(
|
|
1427
|
-
color.dim(
|
|
1428
|
-
`${staleBaselineKeys} baseline entr(y/ies) no longer occur — tighten the ratchet with: ark-check --update-baseline`
|
|
1429
|
-
)
|
|
1430
|
-
);
|
|
1431
|
-
}
|
|
1432
|
-
if (policyDelta && !policyDelta.valid) {
|
|
1433
|
-
for (const finding of policyDelta.findings.filter(
|
|
1434
|
-
({ classification }) =>
|
|
1435
|
-
classification === 'weakening' || classification === 'judgment-required'
|
|
1436
|
-
)) {
|
|
1437
|
-
console.error(
|
|
1438
|
-
`${color.red('policy')} ${finding.classification} ${finding.path}: ${finding.message}`
|
|
1439
|
-
);
|
|
1440
|
-
console.error(` Next: ${finding.nextAction}`);
|
|
1441
|
-
}
|
|
1442
|
-
console.error(
|
|
1443
|
-
`Policy transition blocked (${policyDelta.basePolicyHash} → ${policyDelta.candidatePolicyHash}). ` +
|
|
1444
|
-
'Provide --policy-ack with the exact hashes, finding ids, and a non-empty reason.'
|
|
1445
|
-
);
|
|
1446
|
-
}
|
|
1447
|
-
if (activeViolations.length === 0 && (policyDelta?.valid ?? true)) {
|
|
1448
|
-
const advisoryOnly = warnings.length > 0 && strictWarnings.length === 0;
|
|
1449
|
-
if (warnings.length === 0) {
|
|
1450
|
-
console.log(`${color.green('✔')} Ark check passed.${baselineNote}`);
|
|
1451
|
-
} else if (args.strictConfig && strictWarnings.length > 0) {
|
|
1452
|
-
console.error(
|
|
1453
|
-
`${color.red('✖')} Ark check failed with ${strictWarnings.length} config warning(s).${baselineNote}`
|
|
1454
|
-
);
|
|
1455
|
-
} else if (advisoryOnly) {
|
|
1456
|
-
console.log(
|
|
1457
|
-
`${color.green('✔')} Ark check passed with ${warnings.length} advisory warning(s).${baselineNote}`
|
|
1458
|
-
);
|
|
1459
|
-
} else {
|
|
1460
|
-
console.log(
|
|
1461
|
-
`${color.green('✔')} Ark check passed with ${warnings.length} config warning(s).${baselineNote}`
|
|
1462
|
-
);
|
|
1463
|
-
}
|
|
1464
|
-
} else {
|
|
1465
|
-
console.error(
|
|
1466
|
-
activeViolations.length > 0
|
|
1467
|
-
? `${color.red('✖')} ${activeViolations.length} violation(s).${baselineNote}`
|
|
1468
|
-
: `${color.red('✖')} Policy transition rejected.${baselineNote}`
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// On a large violation set, print the ranked edge breakdown so the wall of failures reads
|
|
1473
|
-
// as an ordered burn-down (and flags a concentrated edge as a likely contract bug).
|
|
1474
|
-
if (activeViolations.length >= CONCENTRATION_MIN_VIOLATIONS) {
|
|
1475
|
-
printViolationBreakdown(summarizeViolations(activeViolations), { toStderr: true });
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
printSkillAndCodexGapHints(root, {
|
|
1479
|
-
skillGaps,
|
|
1480
|
-
codexHomeGap,
|
|
1481
|
-
codexRepoSkillGap,
|
|
1482
|
-
codexSessionActive,
|
|
1483
|
-
color,
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
const staleRunners = staleRunnerGateFiles(root);
|
|
1487
|
-
if (staleRunners.length > 0) {
|
|
1488
|
-
console.log(
|
|
1489
|
-
color.dim(
|
|
1490
|
-
`Ark commands in ${staleRunners.join(', ')} use a runner that doesn't match this repo's ` +
|
|
1491
|
-
`package manager. Fix (no clobber): ${arkCommand(root, 'ark-check', '--install-agent-gates --migrate-commands')}`
|
|
1492
|
-
)
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
const brokenMcp = brokenMcpGateFiles(root);
|
|
1497
|
-
if (brokenMcp.length > 0) {
|
|
1498
|
-
console.log(
|
|
1499
|
-
color.yellow(
|
|
1500
|
-
`Broken MCP argv in ${brokenMcp.join(', ')}: more than one of ark-mcp/arkgate-mcp in args ` +
|
|
1501
|
-
`(stdio hosts get a double binary name). Fix: ${arkCommand(root, 'ark-check', '--install-agent-gates --migrate-commands')}`
|
|
1502
|
-
)
|
|
1503
|
-
);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
if (args.watch) {
|
|
1508
|
-
await runWatchMode(args);
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
process.exitCode = ok ? 0 : 1;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
async function runWatchMode(args) {
|
|
1516
|
-
const argv = process.argv.slice(2).filter((token) => token !== '--watch');
|
|
1517
|
-
let debounce;
|
|
1518
|
-
const rerun = () => {
|
|
1519
|
-
clearTimeout(debounce);
|
|
1520
|
-
debounce = setTimeout(() => {
|
|
1521
|
-
const result = spawnSync(process.execPath, [__arkCheckCli, ...argv], {
|
|
1522
|
-
cwd: args.root,
|
|
1523
|
-
stdio: 'inherit',
|
|
1524
|
-
env: process.env,
|
|
1525
|
-
});
|
|
1526
|
-
process.exitCode = result.status ?? 1;
|
|
1527
|
-
}, 300);
|
|
1528
|
-
};
|
|
1529
|
-
|
|
1530
|
-
let config;
|
|
1531
|
-
try {
|
|
1532
|
-
config = readConfig(args.root, args.config);
|
|
1533
|
-
} catch (error) {
|
|
1534
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
1535
|
-
process.exitCode = 2;
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
for (const entry of config.include ?? []) {
|
|
1540
|
-
const target = path.join(args.root, entry);
|
|
1541
|
-
if (!fs.existsSync(target)) continue;
|
|
1542
|
-
try {
|
|
1543
|
-
const watcher = fs.watch(target, { recursive: true }, rerun);
|
|
1544
|
-
watcher.on('error', () => {
|
|
1545
|
-
watcher.close();
|
|
1546
|
-
watchByPolling(target, rerun);
|
|
1547
|
-
});
|
|
1548
|
-
} catch {
|
|
1549
|
-
watchByPolling(target, rerun);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
console.log(color.dim('Watching governed paths for changes… (Ctrl+C to stop)'));
|
|
1554
|
-
await new Promise(() => {});
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function watchByPolling(target, onChange) {
|
|
1558
|
-
let previous = watchFingerprint(target);
|
|
1559
|
-
setInterval(() => {
|
|
1560
|
-
const current = watchFingerprint(target);
|
|
1561
|
-
if (current === previous) return;
|
|
1562
|
-
previous = current;
|
|
1563
|
-
onChange();
|
|
1564
|
-
}, 250);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
function watchFingerprint(target) {
|
|
1568
|
-
const pending = [target];
|
|
1569
|
-
const entries = [];
|
|
1570
|
-
while (pending.length > 0) {
|
|
1571
|
-
const current = pending.pop();
|
|
1572
|
-
let stat;
|
|
1573
|
-
try {
|
|
1574
|
-
stat = fs.statSync(current);
|
|
1575
|
-
} catch {
|
|
1576
|
-
continue;
|
|
1577
|
-
}
|
|
1578
|
-
entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
|
|
1579
|
-
if (!stat.isDirectory()) continue;
|
|
1580
|
-
try {
|
|
1581
|
-
for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
|
|
1582
|
-
} catch {
|
|
1583
|
-
// A concurrent delete is represented by the next fingerprint.
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
return entries.sort().join('|');
|
|
50
|
+
// The lightweight attempt already proved the resident unavailable. Preserve the exact
|
|
51
|
+
// one-shot fallback without paying a second socket timeout in the full runtime.
|
|
52
|
+
process.argv = process.argv.filter((argument, index) => index < 2 || argument !== '--resident');
|
|
53
|
+
await import('./ark-check-runtime.mjs');
|
|
1587
54
|
}
|
|
1588
55
|
|
|
1589
56
|
main().catch((error) => {
|