arkgate 2.8.3 → 2.9.1

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 (55) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +2 -1
  3. package/bin/ark-check.mjs +21 -3
  4. package/bin/ark-layer-match.mjs +88 -5
  5. package/bin/ark-mcp.mjs +7 -70
  6. package/bin/ark-shared.mjs +88 -8
  7. package/bin/ark.mjs +134 -22
  8. package/bin/lib/agent-gates.mjs +37 -3
  9. package/bin/lib/architecture-scan.mjs +16 -4
  10. package/bin/lib/config-warnings.mjs +11 -2
  11. package/bin/lib/doctor-plan.mjs +20 -0
  12. package/bin/lib/field-install.mjs +334 -0
  13. package/bin/lib/import-resolve.mjs +133 -0
  14. package/bin/lib/presets.mjs +207 -11
  15. package/bin/lib/remediation.mjs +15 -0
  16. package/bin/lib/suggestions.mjs +8 -3
  17. package/dist/eslint/index.cjs +63 -5
  18. package/dist/eslint/index.cjs.map +1 -1
  19. package/dist/eslint/index.d.cts +33 -1
  20. package/dist/eslint/index.d.ts +33 -1
  21. package/dist/eslint/index.js +63 -5
  22. package/dist/eslint/index.js.map +1 -1
  23. package/dist/index.cjs +103 -14
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +21 -9
  26. package/dist/index.d.ts +21 -9
  27. package/dist/index.js +103 -14
  28. package/dist/index.js.map +1 -1
  29. package/dist/nestjs/index.cjs +78 -4
  30. package/dist/nestjs/index.cjs.map +1 -1
  31. package/dist/nestjs/index.d.cts +1 -1
  32. package/dist/nestjs/index.d.ts +1 -1
  33. package/dist/nestjs/index.js +78 -4
  34. package/dist/nestjs/index.js.map +1 -1
  35. package/dist/runtime/index.cjs +103 -14
  36. package/dist/runtime/index.cjs.map +1 -1
  37. package/dist/runtime/index.d.cts +1 -1
  38. package/dist/runtime/index.d.ts +1 -1
  39. package/dist/runtime/index.js +103 -14
  40. package/dist/runtime/index.js.map +1 -1
  41. package/dist/{types-CSJhEOk2.d.cts → types-D6Q8WHes.d.cts} +7 -0
  42. package/dist/{types-CSJhEOk2.d.ts → types-D6Q8WHes.d.ts} +7 -0
  43. package/docs/agent-guide.md +55 -4
  44. package/package.json +3 -1
  45. package/server.json +2 -2
  46. package/templates/architecture-playbook.json +65 -1
  47. package/templates/policy-packs/enthusiast-ddd-bounded-contexts.json +19 -0
  48. package/templates/policy-packs/enthusiast-ui-surface.json +18 -0
  49. package/templates/policy-packs/enthusiast-vertical-slice.json +18 -0
  50. package/templates/skills/ark-adopt.md +4 -0
  51. package/templates/skills/ark-architect.md +5 -1
  52. package/templates/skills/ark-autopilot.md +10 -1
  53. package/templates/skills/ark-fix.md +3 -0
  54. package/templates/skills/ark-place.md +7 -0
  55. package/templates/skills/ark-think.md +43 -0
package/bin/ark.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  resolveArchetypePreset,
19
19
  resolveOperatingMode,
20
20
  } from './ark-shared.mjs';
21
+ import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
21
22
 
22
23
  const here = path.dirname(fileURLToPath(import.meta.url));
23
24
  const arkCheck = path.join(here, 'ark-check.mjs');
@@ -55,7 +56,7 @@ function parseArgs(argv) {
55
56
  function usage() {
56
57
  return `Usage:
57
58
  ark start [--root <project>] [--yes]
58
- ark init [--root <project>] [--preset hexagonal|layered|feature-sliced|monorepo]
59
+ ark init [--root <project>] [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|clean-architecture|onion-architecture]
59
60
  [--archetype <playbook-id>] [--tools <list>] [--yes] [--force] [--no-strict]
60
61
  ark upgrade [--root <project>] [--no-install] [--no-strict]
61
62
 
@@ -70,23 +71,32 @@ Commands:
70
71
 
71
72
  Options:
72
73
  --yes Non-interactive defaults: create config if needed, install gate templates, run strict check.
74
+ (Also the implicit default when stdin/stdout are not a TTY — agents never hang on prompts.)
73
75
  --force Allow generated files to overwrite existing files.
74
76
  --no-strict Skip the final strict ark-check run.
75
- --no-install (upgrade) Refresh gates/skills only; don't reinstall the package.
77
+ --no-install Skip adding/installing arkgate as a project devDependency (start/upgrade).
76
78
  --preset Start from a named architecture preset instead of detection.
77
79
  --archetype Application shape from templates/architecture-playbook.json (maps to the matching preset).
78
80
  Valid ids: crud-product, api-backend, frontend-surface, library-sdk, cli-utility,
79
- worker-pipeline, event-coordinator, integration-bridge, multi-app-workspace, prototype-spike.
80
- --tools Comma-separated agents to gate (claude,cursor,codex,windsurf,cline,copilot,kiro,roo,continue,gemini).
81
- Omit to auto-detect from each tool's config dir, falling back to claude+cursor+codex.
81
+ worker-pipeline, event-coordinator, integration-bridge, multi-app-workspace, prototype-spike,
82
+ vertical-slice-product, ddd-bounded-contexts.
83
+ --tools Comma-separated agents to gate (claude,cursor,codex,grok,windsurf,cline,copilot,kiro,roo,continue,gemini).
84
+ Omit to auto-detect from each tool's config dir, falling back to claude+cursor+codex+grok.
82
85
 
83
86
  Interactive mode (TTY, no --yes): asks what application shape you are building and maps it to a preset.
87
+ Non-interactive (no TTY): uses the same defaults as --yes — never calls readline on a null interface.
84
88
  `;
85
89
  }
86
90
 
87
- // The package-manager command that adds arkgate@latest as a dev dependency.
88
- function packageInstallArgv(root) {
89
- const spec = 'arkgate@latest';
91
+ // The package-manager command that adds arkgate as a dev dependency.
92
+ // Prefer an explicit version/range when pin already chose one (avoid pin=^2.9.0 then
93
+ // `npm i arkgate@latest` rewriting package.json to a different range).
94
+ function packageInstallArgv(root, versionSpec) {
95
+ const range =
96
+ typeof versionSpec === 'string' && versionSpec.trim()
97
+ ? versionSpec.trim()
98
+ : 'latest';
99
+ const spec = range.startsWith('arkgate@') ? range : `arkgate@${range}`;
90
100
  const pm = detectPackageManager(root);
91
101
  if (pm === 'pnpm') return ['pnpm', ['add', '-D', spec]];
92
102
  if (pm === 'yarn') return ['yarn', ['add', '-D', spec]];
@@ -165,13 +175,49 @@ function isInteractiveTty() {
165
175
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
166
176
  }
167
177
 
178
+ /**
179
+ * True when prompts should be skipped and guided defaults applied.
180
+ * Agents typically have no TTY — never call readline on a null interface.
181
+ */
182
+ export function shouldUseNonInteractiveDefaults(args, tty = isInteractiveTty()) {
183
+ return Boolean(args?.yes || !tty);
184
+ }
185
+
168
186
  async function askYesNo(rl, question, defaultYes = true) {
187
+ if (!rl) {
188
+ // Defensive: non-TTY callers must not reach here; return the default rather than throw.
189
+ return defaultYes;
190
+ }
169
191
  const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
170
192
  const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
171
193
  if (!answer) return defaultYes;
172
194
  return answer === 'y' || answer === 'yes';
173
195
  }
174
196
 
197
+ /**
198
+ * Pin arkgate in package.json (and optionally run the package manager).
199
+ * start calls this so CI/`npx` is not forced to rely on a stale global install.
200
+ *
201
+ * @param {string} root
202
+ * @param {{ install?: boolean, runPackageManager?: boolean }} [opts]
203
+ */
204
+ export function ensureProjectArkgateDependency(root, opts = {}) {
205
+ const install = opts.install !== false;
206
+ const runPm = opts.runPackageManager === true;
207
+ if (!install) {
208
+ return { pinned: { changed: false, reason: 'skipped-no-install' }, installStatus: null };
209
+ }
210
+ const pinned = pinArkgateDevDependency(root);
211
+ let installStatus = null;
212
+ // Only run the package manager after a successful pin change — avoid surprise
213
+ // network on every start when arkgate is already listed.
214
+ if (runPm && pinned.changed) {
215
+ const [command, commandArgs] = packageInstallArgv(root, pinned.version);
216
+ installStatus = runCommand(command, commandArgs, root);
217
+ }
218
+ return { pinned, installStatus };
219
+ }
220
+
175
221
  async function resolveArchetypeInteractive(rl, root) {
176
222
  console.log('');
177
223
  console.log('What are you building? (application shape — not a framework name)');
@@ -213,12 +259,19 @@ function resolveInitPreset(args) {
213
259
  async function init(args) {
214
260
  const root = args.root;
215
261
  const configPath = path.join(root, 'ark.config.json');
216
- const interactive = !args.yes && isInteractiveTty();
262
+ const nonInteractive = shouldUseNonInteractiveDefaults(args);
263
+ const interactive = !nonInteractive;
217
264
  const rl = interactive
218
265
  ? readline.createInterface({ input: process.stdin, output: process.stdout })
219
266
  : null;
220
267
 
221
268
  try {
269
+ if (nonInteractive && !args.yes) {
270
+ console.log(
271
+ 'Non-interactive session (no TTY) — using guided defaults (same as --yes). Pass flags to override.'
272
+ );
273
+ }
274
+
222
275
  let archetype = args.archetype;
223
276
  let preset = args.preset;
224
277
 
@@ -230,9 +283,8 @@ async function init(args) {
230
283
  const resolved = resolveArchetypePreset(archetype);
231
284
  preset = resolved.preset;
232
285
  console.log(`Using archetype ${archetype} → preset ${preset} (${resolved.label})`);
233
- } else if (!preset && !interactive && !args.yes) {
234
- // non-TTY without --yes/--preset/--archetype: fall back to detection init
235
- } else if (!preset && args.yes && !archetype) {
286
+ } else if (!preset && nonInteractive && !archetype) {
287
+ // non-TTY / --yes without explicit shape: recommend → preset
236
288
  const rec = buildArchitectureRecommendation(root);
237
289
  preset = rec.preset;
238
290
  archetype = rec.archetype;
@@ -243,7 +295,7 @@ async function init(args) {
243
295
  if (fs.existsSync(configPath)) {
244
296
  shouldInit = args.force
245
297
  ? true
246
- : args.yes
298
+ : nonInteractive
247
299
  ? false
248
300
  : await askYesNo(rl, 'ark.config.json already exists. Regenerate it?', false);
249
301
  }
@@ -258,7 +310,8 @@ async function init(args) {
258
310
  console.log('Skipped ark.config.json generation.');
259
311
  }
260
312
 
261
- const installGates = args.yes || (await askYesNo(rl, 'Configure agent and CI gate templates?', true));
313
+ const installGates =
314
+ nonInteractive || (await askYesNo(rl, 'Configure agent and CI gate templates?', true));
262
315
  if (installGates) {
263
316
  const gateArgs = ['--root', root, '--install-agent-gates'];
264
317
  if (args.tools) gateArgs.push('--tools', args.tools);
@@ -268,7 +321,8 @@ async function init(args) {
268
321
  }
269
322
 
270
323
  const runStrict =
271
- args.strict && (args.yes || (await askYesNo(rl, 'Run strict architecture check now?', true)));
324
+ args.strict &&
325
+ (nonInteractive || (await askYesNo(rl, 'Run strict architecture check now?', true)));
272
326
  if (runStrict) {
273
327
  return runArkCheck(
274
328
  ['--root', root, '--config', 'ark.config.json', '--strict-config'],
@@ -298,7 +352,8 @@ async function init(args) {
298
352
  // orchestrates existing steps (recommend → init → --plan) and frames each in outcome terms.
299
353
  async function start(args) {
300
354
  const root = args.root;
301
- const interactive = !args.yes && isInteractiveTty();
355
+ const nonInteractive = shouldUseNonInteractiveDefaults(args);
356
+ const interactive = !nonInteractive;
302
357
  const rl = interactive
303
358
  ? readline.createInterface({ input: process.stdin, output: process.stdout })
304
359
  : null;
@@ -309,6 +364,11 @@ async function start(args) {
309
364
  "I'll look at your code, suggest a shape, set up the guardrails, and show you a plan."
310
365
  );
311
366
  console.log('Nothing in your code is changed — this only adds Ark configuration.');
367
+ if (nonInteractive && !args.yes) {
368
+ console.log(
369
+ 'Non-interactive session (no TTY) — using guided defaults (same as --yes). Pass flags to override.'
370
+ );
371
+ }
312
372
 
313
373
  // 1) Look at the project.
314
374
  let rec;
@@ -331,7 +391,8 @@ async function start(args) {
331
391
  );
332
392
  console.log('match the contract to how your code is already organized, and flag only genuine issues.');
333
393
  }
334
- const proceed = args.yes || (await askYesNo(rl, '\nSet Ark up for this shape?', true));
394
+ const proceed =
395
+ nonInteractive || (await askYesNo(rl, '\nSet Ark up for this shape?', true));
335
396
  if (!proceed) {
336
397
  archetype = interactive ? await resolveArchetypeInteractive(rl, root) : rec.archetype;
337
398
  }
@@ -339,6 +400,26 @@ async function start(args) {
339
400
  archetype = await resolveArchetypeInteractive(rl, root);
340
401
  }
341
402
 
403
+ // 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
404
+ if (args.install !== false && fs.existsSync(path.join(root, 'package.json'))) {
405
+ const { pinned, installStatus } = ensureProjectArkgateDependency(root, {
406
+ install: true,
407
+ runPackageManager: true,
408
+ });
409
+ if (pinned.changed) {
410
+ console.log(` Pinned arkgate@${pinned.version} in package.json devDependencies.`);
411
+ if (installStatus !== null && installStatus !== 0) {
412
+ console.log(
413
+ ` Package manager install exited ${installStatus} — package.json is still pinned; run install when online.`
414
+ );
415
+ }
416
+ } else if (pinned.reason === 'already-present') {
417
+ console.log(` arkgate already in package.json (${pinned.version}).`);
418
+ }
419
+ } else if (args.install === false) {
420
+ console.log(' Skipping arkgate package pin (--no-install).');
421
+ }
422
+
342
423
  // 3) Set up config + gates. Greenfield → the shape's preset; an established repo → detection,
343
424
  // so the contract anchors to the directories you already have instead of aspirational globs.
344
425
  console.log('');
@@ -472,8 +553,28 @@ async function start(args) {
472
553
 
473
554
  // 5) Plain-language wrap-up — one next step, status light only.
474
555
  // Modes are detected (Suggest/Adapt/Enforce), not user-picked settings.
556
+ // Soft-block false-green using the same doctor adoption gap (no second detector).
557
+ let falseGreenGap = null;
558
+ try {
559
+ const doc = JSON.parse(doctorCapture.stdout || '{}');
560
+ falseGreenGap = (doc.doctor?.adoption?.gaps ?? []).find(
561
+ (g) => g?.id === FALSE_GREEN_GAP_ID
562
+ );
563
+ } catch {
564
+ falseGreenGap = null;
565
+ }
566
+ if (falseGreenGap && mode === 'enforce') {
567
+ mode = 'adapt';
568
+ planOk = false;
569
+ }
570
+
475
571
  console.log('');
476
- if (mode === 'enforce' && planOk) {
572
+ if (falseGreenGap) {
573
+ console.log('Done — status: ADAPT (contract may be a false green — do not stop at a clean plan).');
574
+ console.log('What happens now:');
575
+ console.log(` • ${falseGreenGap.message}`);
576
+ console.log(` • Next: ${falseGreenGap.fix}`);
577
+ } else if (mode === 'enforce' && planOk) {
477
578
  console.log('Done — status: ENFORCE (gates can honestly protect you).');
478
579
  console.log('What happens now:');
479
580
  console.log(' • Every edit is checked (in CI and, if wired, at write time).');
@@ -494,11 +595,16 @@ async function start(args) {
494
595
  }
495
596
  console.log('');
496
597
  console.log('Next (the only flow you need):');
497
- console.log(' 1. In your agent: /ark-autopilot');
498
- console.log(' origin report, adoption, plan, safe fixes, leave gates on.');
598
+ if (falseGreenGap) {
599
+ console.log(' 1. In your agent: /ark-adopt (or /ark-contract) fix the contract first');
600
+ console.log(' → reclassify I/O dirs out of Application; then /ark-autopilot for residual debt.');
601
+ } else {
602
+ console.log(' 1. In your agent: /ark-autopilot');
603
+ console.log(' → origin report, adoption, plan, safe fixes, leave gates on.');
604
+ }
499
605
  console.log(` 2. Status anytime: ${arkCommand(root, 'ark-check', '--doctor')}`);
500
606
  console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
501
- if (mode === 'adapt' && planOk) {
607
+ if (mode === 'adapt' && planOk && !falseGreenGap) {
502
608
  console.log(
503
609
  ` 4. When green but cores still optional: ${arkCommand(root, 'ark-check', '--ratchet-cores')} → honest ENFORCE`
504
610
  );
@@ -559,4 +665,10 @@ async function main() {
559
665
  return 2;
560
666
  }
561
667
 
562
- process.exitCode = await main();
668
+ // Only run when executed as the CLI entry (not when imported by unit tests).
669
+ const isMain =
670
+ Boolean(process.argv[1]) &&
671
+ path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
672
+ if (isMain) {
673
+ process.exitCode = await main();
674
+ }
@@ -23,6 +23,7 @@ import {
23
23
  applyFrameworkLayoutOverlays,
24
24
  } from '../ark-shared.mjs';
25
25
  import { CORE_LAYER_NAMES } from './core-ratchet.mjs';
26
+ import { falseGreenAdoptionGap } from './field-install.mjs';
26
27
  import {
27
28
  assessCodexHomeMcp,
28
29
  codexArkBlockHasPreferredBin,
@@ -365,6 +366,18 @@ export function checkArgsForRoot(root, { requireGates = false } = {}) {
365
366
  return `--root . --config ark.config.json --strict-config${baselineFlag}${gatesFlag}`;
366
367
  }
367
368
 
369
+ // Field-install helpers live in field-install.mjs (keep agent-gates scannable).
370
+ // Re-export for callers that already import from this module.
371
+ export {
372
+ ensureBaselineFlagInCheckCommand,
373
+ syncBaselineIntoCheckSurfaces,
374
+ pinArkgateDevDependency,
375
+ IO_DIR_SEGMENTS,
376
+ detectContractFalseGreenRisk,
377
+ FALSE_GREEN_GAP_ID,
378
+ falseGreenAdoptionGap,
379
+ } from './field-install.mjs';
380
+
368
381
  export function packageManager(root) {
369
382
  // CI always require-gates; baseline follows checkArgsForRoot.
370
383
  const checkArgs = checkArgsForRoot(root, { requireGates: true });
@@ -769,10 +782,20 @@ export function resolveTools(args) {
769
782
  if (fs.existsSync(path.join(root, '.gemini'))) detected.add('gemini');
770
783
  // copilot has no reliable directory signal (.github exists in most repos),
771
784
  // so it is explicit-only via --tools.
772
- // No signal at all: fall back to writing the primary tools' templates so a fresh
773
- // project still gets a complete, reviewable starter set.
785
+ // Host signals: Grok Build / xAI agents often have no project `.grok/` yet but
786
+ // set an env marker (or run with GROK_*). Include Grok so skills install there.
787
+ if (
788
+ process.env.GROK_BUILD === '1' ||
789
+ process.env.GROK_BUILD === 'true' ||
790
+ process.env.XAI_GROK === '1' ||
791
+ process.env.XAI_GROK === 'true'
792
+ ) {
793
+ detected.add('grok');
794
+ }
795
+ // No signal at all: fall back to a complete starter set including Grok (field
796
+ // log: default claude+cursor+codex silently omitted Grok skills for Grok hosts).
774
797
  if (detected.size === 0) {
775
- return { tools: new Set(['claude', 'cursor', 'codex']), source: 'default' };
798
+ return { tools: new Set(['claude', 'cursor', 'codex', 'grok']), source: 'default' };
776
799
  }
777
800
  return { tools: detected, source: 'detected' };
778
801
  }
@@ -1539,6 +1562,16 @@ export function collectAdoptionGaps(root, config, coverage) {
1539
1562
  }
1540
1563
  }
1541
1564
 
1565
+ // --- False-green contract (field-install detector; doctor skillGaps already cover missing skills) ---
1566
+ let contractFalseGreen = null;
1567
+ if (!isProducer && config) {
1568
+ const gap = falseGreenAdoptionGap(root, config, coverage);
1569
+ if (gap) {
1570
+ contractFalseGreen = { risk: true, message: gap.message, fix: gap.fix };
1571
+ gaps.push(gap);
1572
+ }
1573
+ }
1574
+
1542
1575
  return {
1543
1576
  gaps,
1544
1577
  hosts,
@@ -1549,6 +1582,7 @@ export function collectAdoptionGaps(root, config, coverage) {
1549
1582
  baseline,
1550
1583
  layerBalance,
1551
1584
  deployPath,
1585
+ contractFalseGreen,
1552
1586
  };
1553
1587
  }
1554
1588
 
@@ -237,10 +237,16 @@ export function runArchitectureScan({ root, config, manifest, rules, files, ts,
237
237
  importGraph.get(relFile).add(relTarget);
238
238
  }
239
239
  }
240
- const rule = targetLayer ? isBlocked(rules, sourceLayer, targetLayer) : undefined;
240
+ const relTarget = target ? normalize(path.relative(root, target)) : undefined;
241
+ const rule = targetLayer
242
+ ? isBlocked(rules, sourceLayer, targetLayer, {
243
+ fromPath: relFile,
244
+ toPath: relTarget,
245
+ layers: config.layers,
246
+ })
247
+ : undefined;
241
248
  if (rule) {
242
- const relTarget = normalize(path.relative(root, target));
243
- const targetCached = nextCacheFiles[relTarget];
249
+ const targetCached = relTarget ? nextCacheFiles[relTarget] : undefined;
244
250
  const staticEdge = edge.kind === 'import' || edge.kind === 'export';
245
251
  const targetTypeOnlyExports =
246
252
  staticEdge && Boolean(targetCached?.exportsOnlyTypes) && !edge.typeOnly;
@@ -257,6 +263,7 @@ export function runArchitectureScan({ root, config, manifest, rules, files, ts,
257
263
  targetTypeNames.size > 0 &&
258
264
  !targetCached?.hasTopLevelSideEffects &&
259
265
  named.every((n) => targetTypeNames.has(n));
266
+ const peerIsolation = Boolean(rule.peerIsolation);
260
267
  violations.push({
261
268
  ruleId: 'LAYER_IMPORT_VIOLATION',
262
269
  file: relFile,
@@ -269,7 +276,12 @@ export function runArchitectureScan({ root, config, manifest, rules, files, ts,
269
276
  ...(sourcePureTypeModule ? { sourcePureTypeModule: true } : {}),
270
277
  ...(namedBindingsTypeOnly ? { namedBindingsTypeOnly: true } : {}),
271
278
  ...(edge.kind ? { edgeKind: edge.kind } : {}),
272
- message: rule.message ?? `${sourceLayer} must not ${edge.kind} ${targetLayer}.`,
279
+ ...(peerIsolation ? { peerIsolation: true } : {}),
280
+ message:
281
+ rule.message ??
282
+ (peerIsolation
283
+ ? `${sourceLayer} must not ${edge.kind} another slice of ${targetLayer} (${relFile} → ${relTarget}). Extract shared code or use events/ports across slices.`
284
+ : `${sourceLayer} must not ${edge.kind} ${targetLayer}.`),
273
285
  });
274
286
  }
275
287
  }
@@ -10,6 +10,7 @@ import {
10
10
  patternSpecificity,
11
11
  resolveIntentLayer,
12
12
  } from '../ark-shared.mjs';
13
+ import { findDeniedEdgeRule } from '../ark-layer-match.mjs';
13
14
  import { normalize } from './scan-files.mjs';
14
15
 
15
16
  export function intentLayersFromManifest(manifest) {
@@ -36,8 +37,16 @@ export function layerForIntent(intent, layers, manifestIntentLayers) {
36
37
  return resolveIntentLayer(intent, source);
37
38
  }
38
39
 
39
- export function isBlocked(rules, from, to) {
40
- return rules.find((rule) => !rule.allowed && rule.from === from && rule.to === to);
40
+ /**
41
+ * First denying edge rule for from→to, or undefined.
42
+ * Path-aware when options.fromPath / options.toPath are set (peerIsolation).
43
+ * @param {object[]} rules
44
+ * @param {string} from
45
+ * @param {string} to
46
+ * @param {{ fromPath?: string, toPath?: string, layers?: object[] }} [options]
47
+ */
48
+ export function isBlocked(rules, from, to, options) {
49
+ return findDeniedEdgeRule(rules, from, to, options);
41
50
  }
42
51
 
43
52
  export function configWarning(ruleId, message, extra = {}) {
@@ -330,6 +330,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
330
330
  archetype: recommendation?.archetype,
331
331
  label: recommendation?.label,
332
332
  preset: recommendation?.preset,
333
+ galleryStarter: recommendation?.galleryStarter,
334
+ policyPack: recommendation?.policyPack,
333
335
  recommendCommand: arkCommand(root, 'ark-check', '--recommend'),
334
336
  initCommand: recommendation?.archetype
335
337
  ? arkCommand(root, 'ark', `init --archetype ${recommendation.archetype} --yes`)
@@ -409,6 +411,24 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
409
411
  console.log(color.bold('New here?'));
410
412
  if (recommendation) {
411
413
  line(warn, `Suggested application shape: ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
414
+ if (recommendation.galleryStarter) {
415
+ line(ok, `Gallery starter: ${recommendation.galleryStarter}`);
416
+ }
417
+ if (recommendation.policyPack) {
418
+ line(ok, `Policy pack: ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
419
+ }
420
+ if (recommendation.signals?.nestFramework) {
421
+ line(
422
+ ok,
423
+ 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)'
424
+ );
425
+ }
426
+ if (recommendation.signals?.monorepoTooling?.length) {
427
+ line(
428
+ ok,
429
+ `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`
430
+ );
431
+ }
412
432
  } else {
413
433
  line(warn, 'Low governed coverage or fresh config — pick an application shape before adding code.');
414
434
  }