arkgate 4.6.5 → 4.6.7

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +72 -2106
  2. package/README.md +11 -9
  3. package/bin/ark-check-runtime.mjs +36 -332
  4. package/bin/ark-mcp-runtime.mjs +7 -323
  5. package/bin/ark-shared.mjs +24 -158
  6. package/bin/ark.mjs +13 -3
  7. package/bin/lib/adoption-stance.mjs +104 -0
  8. package/bin/lib/check-args.mjs +173 -0
  9. package/bin/lib/check-config-detect.mjs +101 -0
  10. package/bin/lib/check-watch.mjs +80 -0
  11. package/bin/lib/ci-merge-boundary.mjs +4 -2
  12. package/bin/lib/deep-module-coach.mjs +3 -0
  13. package/bin/lib/design-delta.mjs +2 -2
  14. package/bin/lib/design-smells.mjs +1 -1
  15. package/bin/lib/diagnostic-catalog.mjs +1 -1
  16. package/bin/lib/doctor-advisories.mjs +2 -2
  17. package/bin/lib/doctor-human.mjs +509 -0
  18. package/bin/lib/doctor-next-actions.mjs +20 -2
  19. package/bin/lib/doctor-plan.mjs +86 -456
  20. package/bin/lib/enforcement-honesty.mjs +70 -0
  21. package/bin/lib/first-run-help.mjs +8 -7
  22. package/bin/lib/github-enforcement.mjs +22 -9
  23. package/bin/lib/html-report-advisories.mjs +10 -2
  24. package/bin/lib/html-report.mjs +26 -9
  25. package/bin/lib/mcp-adoption.mjs +19 -0
  26. package/bin/lib/mcp-hook-payload.mjs +328 -0
  27. package/bin/lib/package-manager.mjs +174 -0
  28. package/bin/lib/policy-delta-io.mjs +5 -1
  29. package/bin/lib/post-green-path.mjs +5 -1
  30. package/bin/lib/product-copy.mjs +6 -3
  31. package/bin/lib/start-preview.mjs +12 -22
  32. package/bin/lib/status-command.mjs +16 -0
  33. package/bin/lib/status-manifest.mjs +8 -2
  34. package/bin/lib/team-parliament-io.mjs +66 -2
  35. package/bin/lib/team-parliament.mjs +25 -5
  36. package/bin/lib/unavailable-analysis.mjs +1 -0
  37. package/dist/index.cjs +2 -2
  38. package/dist/index.d.ts +10 -2
  39. package/dist/index.js +2 -2
  40. package/docs/README.md +6 -10
  41. package/docs/ai-gates.md +12 -5
  42. package/docs/configuration.md +9 -1
  43. package/docs/diagnostics.md +2 -2
  44. package/docs/package-surface.md +6 -4
  45. package/docs/product-voice.md +6 -4
  46. package/docs/threat-model.md +2 -2
  47. package/docs/use.md +5 -4
  48. package/package.json +1 -1
  49. package/schemas/ark.design-delta.schema.json +1 -1
  50. package/server.json +2 -2
  51. package/templates/agent-skills/README.md +1 -1
package/README.md CHANGED
@@ -16,9 +16,11 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.6.5** is current (on npm after this release). Adopt/place honesty for existing Next.js
20
- > trees, stable doctor JSON, writePath/CI honesty file, hosts-keep upgrade, CLAUDE.md projection.
21
- > [4.6.5 notes](docs/releases/4.6.5.md) · [4.6.4](docs/releases/4.6.4.md) · [4.6.3](docs/releases/4.6.3.md) · [4.6.2](docs/releases/4.6.2.md) · [4.6.1](docs/releases/4.6.1.md) · [4.6.0](docs/releases/4.6.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
19
+ > **ArkGate 4.6.7** is current on npm `latest`.
20
+ > A tree is **adopted** only with a required GitHub status running `arkgate-check --strict-merge`,
21
+ > or `.ark/adoption-stance.json` `stance: "advisory-only"`. Doctor is compact (`--doctor --all`
22
+ > for Details). [4.6.7 notes](docs/releases/4.6.7.md) · [4.6.6](docs/releases/4.6.6.md) ·
23
+ > [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
22
24
 
23
25
  ---
24
26
 
@@ -41,12 +43,11 @@ npm install -D arkgate typescript
41
43
  npx arkgate start # preview files + commands
42
44
  npx arkgate start --apply # compact contract + host router + CI plan
43
45
  npx arkgate-check --doctor # control plane: status light + primary next action
46
+ npx arkgate-check --doctor --all # encyclopedia (Details)
44
47
  ```
45
48
 
46
49
  That is the product. Doctor is the control plane — when stuck, do **primary next action #1**.
47
- Doctor also shows **what to improve next** (architecture lenses such as separation of concerns and
48
- dependency inversion — **not a score**). Leftover lenses mean design work may remain even when
49
- imports are green. Details: [use.md — Improvement compass](docs/use.md#improvement-compass-not-a-score).
50
+ JSON still carries improvement compass and coach (**not a score**). Compact human output does not.
50
51
 
51
52
  ```text
52
53
  start → doctor (+ compass) → /ark-adopt (session 0) → day-to-day /ark-place
@@ -102,7 +103,7 @@ check: [4.3.0 — What ArkGate is / isn't](docs/releases/4.3.0.md#what-arkgate-i
102
103
  |-------|--------|-----------|
103
104
  | **Suggest** | Thin / new tree | Finish `start` → doctor |
104
105
  | **Adapt** | Not fully protected | Doctor action #1 |
105
- | **Enforce** | Honest edges under the contract | Keep write path + CI |
106
+ | **Enforce** | Honest import edges, and no new UI business-rule files vs merge-base | Keep write path + CI |
106
107
  | **Enforce · design-weak** | Edges clean; design residual remains | Shape residual — not “done” |
107
108
 
108
109
  Details: [docs/use.md](docs/use.md).
@@ -216,8 +217,9 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
216
217
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
217
218
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
218
219
  | Security | [SECURITY.md](SECURITY.md) |
219
- | Current release (4.6.5) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) · [CHANGELOG](CHANGELOG.md) |
220
- | Prior published (4.6.4 on npm `latest` until 4.6.5 lands) | [docs/releases/4.6.4.md](docs/releases/4.6.4.md) |
220
+ | Current published (4.6.7 on npm `latest`) | [docs/releases/4.6.7.md](docs/releases/4.6.7.md) · [CHANGELOG](CHANGELOG.md) |
221
+ | Prior published (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) |
222
+ | Prior published (4.6.5) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) |
221
223
  | Prior published (4.6.3) | [docs/releases/4.6.3.md](docs/releases/4.6.3.md) |
222
224
  | Prior (4.6.2) | [docs/releases/4.6.2.md](docs/releases/4.6.2.md) |
223
225
  | Prior (4.6.1) | [docs/releases/4.6.1.md](docs/releases/4.6.1.md) |
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'node:url';
7
7
  const __arkCheckCli = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
8
8
 
9
9
  import {
10
- DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
11
10
  DEFAULT_INTENT_PREFIXES,
12
11
  DEFAULT_LAYER_DIRECTORIES,
13
12
  DEFAULT_RULES,
@@ -80,7 +79,6 @@ import {
80
79
  CONCENTRATION_MIN_VIOLATIONS,
81
80
  } from './lib/violations.mjs';
82
81
  import {
83
- suggestLayerForDir,
84
82
  detectBestFitModel,
85
83
  dirSegmentsFromGlob,
86
84
  } from './lib/suggestions.mjs';
@@ -105,10 +103,15 @@ import { runArchitectureScan } from './lib/architecture-scan.mjs';
105
103
  import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement } from './lib/analysis-completeness.mjs';
106
104
  import { reportUnavailableAnalysis } from './lib/unavailable-analysis.mjs';
107
105
  import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
108
- import { analyzePolicyTransition } from './lib/policy-delta-io.mjs';
106
+ import {
107
+ analyzePolicyTransition,
108
+ discoverLocalBaseRef,
109
+ normalizePolicyBaseRef,
110
+ } from './lib/policy-delta-io.mjs';
109
111
  import {
110
112
  applyAgainstRatchet,
111
113
  bindTeamBaseRefs,
114
+ contractSessionFrom,
112
115
  filterChangedGovernedFiles,
113
116
  runTeamPreflight,
114
117
  ungovernedDumpMessage,
@@ -121,164 +124,9 @@ import {
121
124
  resolveEffectiveProjectRoot,
122
125
  } from './lib/project-root.mjs';
123
126
  import { demoteArkRuleTeethUnderClassificationFloor } from './lib/rules-under-contract.mjs';
124
-
125
- function parseArgs(argv) {
126
- const args = {
127
- root: process.cwd(),
128
- config: 'ark.config.json',
129
- manifest: undefined,
130
- printConfig: undefined,
131
- tsconfig: undefined,
132
- json: false,
133
- strictConfig: false,
134
- strictMerge: false,
135
- requireGates: false,
136
- requireWriteHook: undefined,
137
- init: false,
138
- installAgentGates: false,
139
- compact: false,
140
- tools: undefined,
141
- force: false,
142
- skillsOnly: false,
143
- baseline: undefined,
144
- policyBase: undefined,
145
- policyBaseRef: undefined,
146
- policyAck: undefined, failOnNewSmells: false, baseRef: undefined,
147
- contractSession: false,
148
- contractDiff: false,
149
- changed: false,
150
- against: undefined,
151
- base: undefined,
152
- persona: undefined,
153
- author: undefined,
154
- failUngoverned: false,
155
- updateBaseline: false,
156
- noCache: false,
157
- resident: false,
158
- coverage: false,
159
- migrateCommands: false,
160
- doctor: false,
161
- plan: false,
162
- recommend: false,
163
- writePlan: false,
164
- listPolicyPacks: false,
165
- applyPolicyPack: undefined,
166
- watch: false,
167
- beginner: false,
168
- openReport: false,
169
- noOpenReport: false,
170
- version: false,
171
- help: false,
172
- all: false,
173
- followConfigRoot: false,
174
- };
175
- const requireValue = (flag, index) => {
176
- const value = argv[index + 1];
177
- if (value === undefined || value.startsWith('-')) {
178
- throw new Error(`Missing value for ${flag}. Run arkgate-check --help for usage.`);
179
- }
180
- return value;
181
- };
182
- for (let i = 2; i < argv.length; i += 1) {
183
- const arg = argv[i];
184
- if (arg === '--json') args.json = true;
185
- else if (arg === '--strict' || arg === '--strict-merge') {
186
- args.strictConfig = true;
187
- args.requireGates = true;
188
- args.strictMerge = true;
189
- }
190
- else if (arg === '--strict-config') args.strictConfig = true;
191
- else if (arg === '--require-gates') {
192
- args.requireGates = true;
193
- args.strictConfig = true;
194
- }
195
- else if (arg === '--require-write-hook') {
196
- args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
197
- }
198
- else if (arg === '--init') args.init = true;
199
- else if (arg === '--preset') args.preset = requireValue(arg, i++);
200
- else if (arg === '--install-agent-gates') args.installAgentGates = true;
201
- else if (arg === '--compact') args.compact = true;
202
- else if (arg === '--tools') {
203
- // Consume the next arg only when it isn't another flag (same rule as --baseline),
204
- // so `--tools --force` can't silently eat --force as a "tool name".
205
- const next = argv[i + 1];
206
- if (next !== undefined && !next.startsWith('-')) {
207
- i += 1;
208
- args.tools = next
209
- .split(',')
210
- .map((tool) => tool.trim().toLowerCase())
211
- .filter(Boolean);
212
- } else {
213
- args.tools = []; // flag without a value — rejected in runInstallAgentGates
214
- }
215
- }
216
- else if (arg === '--force') args.force = true;
217
- else if (arg === '--follow-config-root') args.followConfigRoot = true;
218
- else if (arg === '--skills-only') args.skillsOnly = true;
219
- else if (arg === '--coverage') args.coverage = true;
220
- else if (arg === '--doctor') args.doctor = true;
221
- else if (arg === '--plan') args.plan = true;
222
- else if (arg === '--rules-inventory') args.rulesInventory = true;
223
- else if (arg === '--recommend') args.recommend = true;
224
- else if (arg === '--write-plan') args.writePlan = true;
225
- else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
226
- else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++);
227
- else if (arg === '--suggest-include') args.suggestInclude = true;
228
- else if (arg === '--adopt-contract') args.adoptContract = true;
229
- else if (arg === '--migrate-contract') args.migrateContract = true;
230
- else if (arg === '--ratchet-cores') args.ratchetCores = true;
231
- else if (arg === '--write') args.write = true;
232
- else if (arg === '--watch') args.watch = true;
233
- else if (arg === '--beginner') args.beginner = true;
234
- else if (arg === '--codex-home') args.codexHome = true;
235
- else if (arg === '--claude-home') args.claudeHome = true;
236
- else if (arg === '--grok-home') args.grokHome = true;
237
- else if (arg === '--agent-homes') {
238
- args.agentHomes = true;
239
- args.codexHome = true;
240
- args.claudeHome = true;
241
- args.grokHome = true;
242
- }
243
- else if (arg === '--migrate-commands') args.migrateCommands = true;
244
- else if (arg === '--no-cache') args.noCache = true;
245
- else if (arg === '--resident') args.resident = true;
246
- else if (arg === '--report') {
247
- const next = argv[i + 1];
248
- args.report = next && !next.startsWith('-') ? argv[++i] : 'ark-report.html';
249
- }
250
- else if (arg === '--reset-origin') args.resetOrigin = true;
251
- else if (arg === '--no-archive') args.noArchive = true;
252
- else if (arg === '--open') args.openReport = true;
253
- else if (arg === '--no-open') args.noOpenReport = true;
254
- else if (arg === '--baseline' || arg === '--update-baseline') {
255
- if (arg === '--update-baseline') args.updateBaseline = true;
256
- // optional path value: consume the next arg only when it isn't another flag
257
- const next = argv[i + 1];
258
- args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
259
- }
260
- else if (arg === '--policy-base') args.policyBase = requireValue(arg, i++);
261
- else if (arg === '--policy-base-ref') args.policyBaseRef = requireValue(arg, i++);
262
- else if (arg === '--policy-ack') args.policyAck = requireValue(arg, i++); else if (arg === '--fail-on-new-smells') args.failOnNewSmells = true; else if (arg === '--base-ref') args.baseRef = requireValue(arg, i++);
263
- else if (arg === '--contract-session') args.contractSession = true;
264
- else if (arg === '--contract-diff') args.contractDiff = true;
265
- else if (arg === '--changed') args.changed = true;
266
- else if (arg === '--against') args.against = requireValue(arg, i++);
267
- else if (arg === '--base') args.base = requireValue(arg, i++);
268
- else if (arg === '--persona') args.persona = requireValue(arg, i++);
269
- else if (arg === '--author') args.author = requireValue(arg, i++);
270
- else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++));
271
- else if (arg === '--config') args.config = requireValue(arg, i++);
272
- else if (arg === '--manifest') args.manifest = requireValue(arg, i++);
273
- else if (arg === '--print-config') args.printConfig = requireValue(arg, i++);
274
- else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++);
275
- else if (arg === '--help' || arg === '-h') args.help = true;
276
- else if (arg === '--all') args.all = true;
277
- else if (arg === '--version' || arg === '-V') args.version = true;
278
- else throw new Error(`Unknown argument: ${arg}. Run arkgate-check --help for usage.`);
279
- }
280
- return args;
281
- }
127
+ import { parseArgs, resolveDesignDeltaBaseRef } from './lib/check-args.mjs';
128
+ import { detectConfig, proposeForUncovered } from './lib/check-config-detect.mjs';
129
+ import { runWatchMode } from './lib/check-watch.mjs';
282
130
 
283
131
  /** Path shown to humans: project-relative when inside root, absolute otherwise (no `../../..`). */
284
132
  function displayPathFromRoot(root, absPath) {
@@ -302,101 +150,6 @@ function readConfig(root, configPath) {
302
150
  return parseArkConfigJson(fs.readFileSync(fullPath, 'utf8'), fullPath).config;
303
151
  }
304
152
 
305
- /**
306
- * Infer an ark.config.json from the directories that actually exist in the project,
307
- * using the same layer→directory conventions as the eleven-layer template. A directory
308
- * only counts when it contains at least one source file, so an empty scaffold dir can't
309
- * produce a layer whose pattern matches nothing (which --strict-config would fail).
310
- */
311
- function detectConfig(root) {
312
- const srcDir = fs.existsSync(path.join(root, 'src')) ? 'src' : '.';
313
- const layers = [];
314
-
315
- for (const entry of DEFAULT_INTENT_PREFIXES) {
316
- const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter(
317
- (directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0
318
- );
319
- if (directories.length === 0) continue;
320
- layers.push({
321
- name: entry.layer,
322
- patterns: directories.map((directory) => `${normalize(path.join(srcDir, directory))}/**`),
323
- intentPrefixes: entry.prefixes,
324
- ...(entry.layer === 'DomainModel'
325
- ? { forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS }
326
- : {}),
327
- });
328
- }
329
-
330
- const names = new Set(layers.map((layer) => layer.name));
331
- const rules = DEFAULT_RULES.filter((rule) => names.has(rule.from) && names.has(rule.to));
332
-
333
- return { srcDir, config: { include: [srcDir], layers, rules } };
334
- }
335
-
336
- /** Top-level directories under srcDir not covered by any detected layer pattern. */
337
- function uncoveredDirectories(root, srcDir, layers) {
338
- const base = path.join(root, srcDir);
339
- if (!fs.existsSync(base)) return [];
340
- return fs
341
- .readdirSync(base, { withFileTypes: true })
342
- .filter(
343
- (entry) =>
344
- entry.isDirectory() &&
345
- entry.name !== 'node_modules' &&
346
- entry.name !== 'dist' &&
347
- !entry.name.startsWith('.')
348
- )
349
- .map((entry) => entry.name)
350
- .filter((name) => {
351
- const prefix = `${normalize(path.join(srcDir, name))}/`;
352
- return !layers.some((layer) =>
353
- layer.patterns.some((pattern) => pattern.startsWith(prefix))
354
- );
355
- });
356
- }
357
-
358
- // detectWorkspaces: shared implementation in ark-shared.mjs (npm/pnpm/rush/lerna +
359
- // conventional multi-package roots).
360
-
361
- // Deny every "upward" edge for an ordered layer list (index 0 = outermost/top,
362
- // which may import everything below it). Inner/lower layers must not import outer
363
- // ones — the shared shape behind linear layered and feature-sliced layouts.
364
-
365
- function proposeForUncovered(root, srcDir, layers) {
366
- const proposals = [];
367
- for (const top of uncoveredDirectories(root, srcDir, layers)) {
368
- const direct = suggestLayerForDir(top);
369
- if (direct) {
370
- proposals.push({ dir: `${srcDir}/${top}`, ...direct });
371
- continue;
372
- }
373
- let children = [];
374
- try {
375
- children = fs
376
- .readdirSync(path.join(root, srcDir, top), { withFileTypes: true })
377
- .filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
378
- .map((e) => e.name);
379
- } catch {
380
- /* not a readable directory — treat as unrecognized below */
381
- }
382
- if (children.length > 0) {
383
- // Descend: propose per child so a mixed `lib/` yields lib/repositories → Persistence
384
- // AND flags lib/db as unrecognized, instead of dropping the parts Ark can't place.
385
- for (const child of children) {
386
- const hit = suggestLayerForDir(child);
387
- proposals.push(
388
- hit
389
- ? { dir: `${srcDir}/${top}/${child}`, ...hit }
390
- : { dir: `${srcDir}/${top}/${child}`, unrecognized: true }
391
- );
392
- }
393
- } else {
394
- proposals.push({ dir: `${srcDir}/${top}`, unrecognized: true });
395
- }
396
- }
397
- return proposals;
398
- }
399
-
400
153
  function printInitNextSteps(root) {
401
154
  console.log('');
402
155
  console.log('Next steps:');
@@ -1324,6 +1077,11 @@ async function main() {
1324
1077
  console.log('✔ Ark check passed (no governed source or constitution files in the diff).');
1325
1078
  } else {
1326
1079
  console.error(preflight.halt.message);
1080
+ const blocking = policyDelta?.findings?.find(
1081
+ (finding) =>
1082
+ finding.classification === 'weakening' || finding.classification === 'judgment-required'
1083
+ );
1084
+ if (blocking?.nextAction) console.error(`Next: ${blocking.nextAction}`);
1327
1085
  }
1328
1086
  process.exitCode = preflight.halt.exitCode;
1329
1087
  return;
@@ -1406,7 +1164,17 @@ async function main() {
1406
1164
  populatedLayerCount,
1407
1165
  });
1408
1166
 
1409
- const designCheck = createDesignDeltaCheck({ enabled: args.failOnNewSmells, root, config, configPath: args.config, baseRef: args.baseRef, ts });
1167
+ const createdPathsOnly = Boolean(args.strictMerge && !args.failOnNewSmells);
1168
+ const designCheck = createDesignDeltaCheck({
1169
+ enabled: args.failOnNewSmells || args.strictMerge,
1170
+ createdPathsOnly,
1171
+ missingBase: createdPathsOnly ? 'skip' : 'fail-closed',
1172
+ root,
1173
+ config,
1174
+ configPath: args.config,
1175
+ baseRef: resolveDesignDeltaBaseRef(root, args.baseRef),
1176
+ ts,
1177
+ });
1410
1178
  const designDelta = designCheck.result;
1411
1179
 
1412
1180
  if (args.doctor) {
@@ -1417,6 +1185,7 @@ async function main() {
1417
1185
  configWalkedUp: args.configWalkedUp === true,
1418
1186
  safety, designDelta,
1419
1187
  ts, parseHealth, completeness,
1188
+ all: args.all === true,
1420
1189
  });
1421
1190
  if (designDelta) process.exitCode = !designDelta.complete ? 2 : designDelta.valid ? 0 : 1; return;
1422
1191
  }
@@ -1427,6 +1196,11 @@ async function main() {
1427
1196
  }
1428
1197
 
1429
1198
  if (args.updateBaseline) {
1199
+ if (!contractSessionFrom(args)) {
1200
+ console.error('Growing the baseline requires --contract-session. Freeze in a law-only PR.');
1201
+ process.exitCode = 1;
1202
+ return;
1203
+ }
1430
1204
  const summary = summarizeViolations(violations);
1431
1205
  // Bloquear y avisar: a lopsided freeze buries a likely contract bug as "debt". Refuse it
1432
1206
  // (unless --force), diagnose, and point at the contract fix instead of the baseline.
@@ -1928,87 +1702,17 @@ async function main() {
1928
1702
  }
1929
1703
 
1930
1704
  if (args.watch) {
1931
- await runWatchMode(args);
1705
+ await runWatchMode(args, {
1706
+ cliPath: __arkCheckCli,
1707
+ loadConfig: readConfig,
1708
+ dim: color.dim,
1709
+ });
1932
1710
  return;
1933
1711
  }
1934
1712
 
1935
1713
  process.exitCode = designCheck.exitCode(observedOk && (!args.strictMerge || analysisComplete) ? 0 : 1);
1936
1714
  }
1937
1715
 
1938
- async function runWatchMode(args) {
1939
- const argv = process.argv.slice(2).filter((token) => token !== '--watch');
1940
- let debounce;
1941
- const rerun = () => {
1942
- clearTimeout(debounce);
1943
- debounce = setTimeout(() => {
1944
- const result = spawnSync(process.execPath, [__arkCheckCli, ...argv], {
1945
- cwd: args.root,
1946
- stdio: 'inherit',
1947
- env: process.env,
1948
- });
1949
- process.exitCode = result.status ?? 1;
1950
- }, 300);
1951
- };
1952
-
1953
- let config;
1954
- try {
1955
- config = readConfig(args.root, args.config);
1956
- } catch (error) {
1957
- console.error(error instanceof Error ? error.message : String(error));
1958
- process.exitCode = 2;
1959
- return;
1960
- }
1961
-
1962
- for (const entry of config.include ?? []) {
1963
- const target = path.join(args.root, entry);
1964
- if (!fs.existsSync(target)) continue;
1965
- try {
1966
- const watcher = fs.watch(target, { recursive: true }, rerun);
1967
- watcher.on('error', () => {
1968
- watcher.close();
1969
- watchByPolling(target, rerun);
1970
- });
1971
- } catch {
1972
- watchByPolling(target, rerun);
1973
- }
1974
- }
1975
-
1976
- console.log(color.dim('Watching governed paths for changes… (Ctrl+C to stop)'));
1977
- await new Promise(() => {});
1978
- }
1979
-
1980
- function watchByPolling(target, onChange) {
1981
- let previous = watchFingerprint(target);
1982
- setInterval(() => {
1983
- const current = watchFingerprint(target);
1984
- if (current === previous) return;
1985
- previous = current;
1986
- onChange();
1987
- }, 250);
1988
- }
1989
-
1990
- function watchFingerprint(target) {
1991
- const pending = [target];
1992
- const entries = [];
1993
- while (pending.length > 0) {
1994
- const current = pending.pop();
1995
- let stat;
1996
- try {
1997
- stat = fs.statSync(current);
1998
- } catch {
1999
- continue;
2000
- }
2001
- entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
2002
- if (!stat.isDirectory()) continue;
2003
- try {
2004
- for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
2005
- } catch {
2006
- // A concurrent delete is represented by the next fingerprint.
2007
- }
2008
- }
2009
- return entries.sort().join('|');
2010
- }
2011
-
2012
1716
  main().catch((error) => {
2013
1717
  console.error(
2014
1718
  process.env.ARK_DEBUG_STACK === '1' && error instanceof Error