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.
- package/CHANGELOG.md +80 -0
- package/README.md +2 -1
- package/bin/ark-check.mjs +21 -3
- package/bin/ark-layer-match.mjs +88 -5
- package/bin/ark-mcp.mjs +7 -70
- package/bin/ark-shared.mjs +88 -8
- package/bin/ark.mjs +134 -22
- package/bin/lib/agent-gates.mjs +37 -3
- package/bin/lib/architecture-scan.mjs +16 -4
- package/bin/lib/config-warnings.mjs +11 -2
- package/bin/lib/doctor-plan.mjs +20 -0
- package/bin/lib/field-install.mjs +334 -0
- package/bin/lib/import-resolve.mjs +133 -0
- package/bin/lib/presets.mjs +207 -11
- package/bin/lib/remediation.mjs +15 -0
- package/bin/lib/suggestions.mjs +8 -3
- package/dist/eslint/index.cjs +63 -5
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.cts +33 -1
- package/dist/eslint/index.d.ts +33 -1
- package/dist/eslint/index.js +63 -5
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +103 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -9
- package/dist/index.d.ts +21 -9
- package/dist/index.js +103 -14
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +78 -4
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +78 -4
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +103 -14
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +103 -14
- package/dist/runtime/index.js.map +1 -1
- package/dist/{types-CSJhEOk2.d.cts → types-D6Q8WHes.d.cts} +7 -0
- package/dist/{types-CSJhEOk2.d.ts → types-D6Q8WHes.d.ts} +7 -0
- package/docs/agent-guide.md +55 -4
- package/package.json +3 -1
- package/server.json +2 -2
- package/templates/architecture-playbook.json +65 -1
- package/templates/policy-packs/enthusiast-ddd-bounded-contexts.json +19 -0
- package/templates/policy-packs/enthusiast-ui-surface.json +18 -0
- package/templates/policy-packs/enthusiast-vertical-slice.json +18 -0
- package/templates/skills/ark-adopt.md +4 -0
- package/templates/skills/ark-architect.md +5 -1
- package/templates/skills/ark-autopilot.md +10 -1
- package/templates/skills/ark-fix.md +3 -0
- package/templates/skills/ark-place.md +7 -0
- 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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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 &&
|
|
234
|
-
// non-TTY
|
|
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
|
-
:
|
|
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 =
|
|
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 &&
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/bin/lib/agent-gates.mjs
CHANGED
|
@@ -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
|
-
//
|
|
773
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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 = {}) {
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -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
|
}
|