arkgate 4.8.20 → 4.8.22
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 +172 -5
- package/README.md +107 -117
- package/bin/ark-check-runtime.mjs +3 -4
- package/bin/ark-layer-match.mjs +110 -18
- package/bin/lib/analysis-engine.mjs +13 -6
- package/bin/lib/config-contract.mjs +5 -0
- package/bin/lib/diagnostic-catalog.mjs +1 -0
- package/bin/lib/doctor-advisories.mjs +20 -0
- package/bin/lib/doctor-human.mjs +11 -9
- package/bin/lib/doctor-plan.mjs +15 -14
- package/bin/lib/first-run-help.mjs +7 -1
- package/bin/lib/html-report-advisories.mjs +8 -0
- package/bin/lib/html-report-depth.mjs +9 -8
- package/bin/lib/invariant-coverage.mjs +417 -72
- package/bin/lib/managed-upgrade.mjs +59 -14
- package/bin/lib/pilot-loop.mjs +183 -37
- package/bin/lib/upgrade-behavior-changes.mjs +83 -0
- package/bin/lib/upgrade-command.mjs +69 -31
- package/bin/lib/upgrade-outcome.mjs +359 -0
- package/bin/lib/violations.mjs +109 -0
- package/dist/{configTypes-VVknqcTJ.d.ts → configTypes-CTELo87-.d.ts} +7 -0
- package/dist/{diagnosticCatalog-BPYWNyJR.d.ts → diagnosticCatalog-WD2TPSwZ.d.ts} +78 -5
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +26 -3
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +32 -25
- package/dist/index.d.ts +9 -4
- package/dist/index.js +33 -26
- package/dist/nestjs/index.cjs +9 -9
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +9 -9
- package/dist/runtime/index.cjs +22 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +22 -15
- package/dist/{types-p_fCp6P-.d.ts → types-fQ9-KrBj.d.ts} +1 -1
- package/dist/{types-Cs-46DRu.d.ts → types-yDNEQ603.d.ts} +1 -1
- package/docs/README.md +5 -3
- package/docs/agent-guide.md +15 -7
- package/docs/brownfield-adoption.md +4 -3
- package/docs/configuration.md +34 -11
- package/docs/diagnostics.md +12 -2
- package/docs/package-surface.md +3 -2
- package/docs/product-voice.md +11 -3
- package/package.json +2 -1
- package/schemas/ark.config.schema.json +8 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -2
- package/templates/agent-skills/ark-architect/SKILL.md +2 -0
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -4
- package/templates/agent-skills/ark-contract/SKILL.md +2 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explain/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +4 -2
- package/templates/agent-skills/ark-fix/SKILL.md +2 -0
- package/templates/agent-skills/ark-loop/SKILL.md +2 -0
- package/templates/agent-skills/ark-order/SKILL.md +2 -0
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +2 -0
- package/templates/agent-skills/ark-think/SKILL.md +2 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +20 -7
- package/templates/skills/ark-adopt.md +5 -2
- package/templates/skills/ark-architect.md +2 -0
- package/templates/skills/ark-autopilot.md +6 -4
- package/templates/skills/ark-contract.md +2 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explain.md +2 -0
- package/templates/skills/ark-explore.md +4 -2
- package/templates/skills/ark-fix.md +2 -0
- package/templates/skills/ark-loop.md +2 -0
- package/templates/skills/ark-order.md +2 -0
- package/templates/skills/ark-place.md +2 -0
- package/templates/skills/ark-runtime.md +2 -0
- package/templates/skills/ark-think.md +2 -0
- package/templates/skills/ark-upgrade.md +20 -7
|
@@ -10,6 +10,13 @@ import {
|
|
|
10
10
|
formatPackageInstallDecisionHuman,
|
|
11
11
|
} from '../ark-shared.mjs';
|
|
12
12
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
13
|
+
import {
|
|
14
|
+
exitCodeFor,
|
|
15
|
+
formatBlockedAssets,
|
|
16
|
+
formatPostUpgradeHuman,
|
|
17
|
+
runPostUpgradeVerification,
|
|
18
|
+
upgradeOutcome,
|
|
19
|
+
} from './upgrade-outcome.mjs';
|
|
13
20
|
import { __packageRoot } from './gate-files.mjs';
|
|
14
21
|
import {
|
|
15
22
|
applyManagedUpgrade,
|
|
@@ -316,15 +323,13 @@ export function buildUpgradeNextCommand(args, planDigest) {
|
|
|
316
323
|
return arkCommand(args.root, 'arkgate', argsStr);
|
|
317
324
|
}
|
|
318
325
|
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
});
|
|
327
|
-
return { exitCode: result.status ?? 1, stderr: result.stderr?.trim() || undefined };
|
|
326
|
+
function stampBlockedNextCommands(plan, args) {
|
|
327
|
+
if ((plan?.summary?.blocked ?? 0) === 0) return plan;
|
|
328
|
+
const command = buildUpgradeNextCommand({ ...args, acceptConflicts: true }, plan.planDigest);
|
|
329
|
+
for (const asset of plan.assets ?? []) {
|
|
330
|
+
if (asset.blocked) asset.nextCommand = command;
|
|
331
|
+
}
|
|
332
|
+
return plan;
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
function runProjectLocalCli({ root, script, argv }) {
|
|
@@ -544,11 +549,14 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
544
549
|
}).status ?? 1;
|
|
545
550
|
}
|
|
546
551
|
|
|
547
|
-
const plan =
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
+
const plan = stampBlockedNextCommands(
|
|
553
|
+
planManagedUpgrade(root, {
|
|
554
|
+
tools: args.tools,
|
|
555
|
+
acceptConflicts: args.acceptConflicts,
|
|
556
|
+
refreshSkills: args.refreshSkills === true,
|
|
557
|
+
}),
|
|
558
|
+
args
|
|
559
|
+
);
|
|
552
560
|
if (!args.apply) {
|
|
553
561
|
const wouldWrite = plan.summary?.wouldWrite ?? 0;
|
|
554
562
|
const blocked = plan.summary?.blocked ?? 0;
|
|
@@ -577,11 +585,14 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
577
585
|
);
|
|
578
586
|
} else {
|
|
579
587
|
renderManagedUpgrade(plan, {
|
|
580
|
-
next:
|
|
581
|
-
|
|
582
|
-
?
|
|
583
|
-
:
|
|
584
|
-
|
|
588
|
+
next:
|
|
589
|
+
blocked > 0
|
|
590
|
+
? undefined
|
|
591
|
+
: needsApply
|
|
592
|
+
? args.install
|
|
593
|
+
? `Update the package and recompute this preview with: ${command}`
|
|
594
|
+
: `Apply the exact preview with: ${command}`
|
|
595
|
+
: undefined,
|
|
585
596
|
skillDrift,
|
|
586
597
|
hostSelection: hostHonesty,
|
|
587
598
|
});
|
|
@@ -600,54 +611,80 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
600
611
|
} catch (error) {
|
|
601
612
|
const message = error instanceof Error ? error.message : String(error);
|
|
602
613
|
console.error(message);
|
|
603
|
-
return
|
|
614
|
+
return exitCodeFor(upgradeOutcome({ error }));
|
|
604
615
|
}
|
|
605
616
|
if (applied.blocked) {
|
|
617
|
+
stampBlockedNextCommands(applied, args);
|
|
606
618
|
const command = buildUpgradeNextCommand(
|
|
607
619
|
{ ...args, acceptConflicts: true },
|
|
608
620
|
applied.planDigest
|
|
609
621
|
);
|
|
622
|
+
const refusal = formatBlockedAssets(applied, { refused: true }).join('\n');
|
|
623
|
+
console.error(refusal);
|
|
610
624
|
if (args.json) {
|
|
611
625
|
console.log(
|
|
612
626
|
managedUpgradeJson(applied, {
|
|
613
627
|
blocked: true,
|
|
628
|
+
outcome: 'blocked',
|
|
614
629
|
reasonCode: applied.reasonCode ?? 'managed-consent-required',
|
|
615
630
|
nextCommand: command,
|
|
616
631
|
})
|
|
617
632
|
);
|
|
618
633
|
} else {
|
|
619
|
-
renderManagedUpgrade(applied, {
|
|
620
|
-
next: 'Preview again with --accept-conflicts, then use that preview\'s exact next command.',
|
|
621
|
-
});
|
|
634
|
+
renderManagedUpgrade(applied, { refused: true });
|
|
622
635
|
}
|
|
623
|
-
return
|
|
636
|
+
return exitCodeFor(upgradeOutcome({ blocked: true }));
|
|
624
637
|
}
|
|
625
638
|
if (applied.nothingToApply && !applied.applied) {
|
|
626
|
-
|
|
639
|
+
const idle = upgradeOutcome({ nothingToApply: true, applied: false });
|
|
640
|
+
if (args.json) console.log(JSON.stringify({ ...applied, outcome: idle.kind }, null, 2));
|
|
627
641
|
else {
|
|
628
642
|
renderManagedUpgrade(applied);
|
|
629
643
|
console.log('No managed content writes pending.');
|
|
630
644
|
}
|
|
631
|
-
return
|
|
645
|
+
return exitCodeFor(idle);
|
|
632
646
|
}
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
:
|
|
647
|
+
const postUpgrade = runPostUpgradeVerification(root, {
|
|
648
|
+
strict: args.strict,
|
|
649
|
+
arkCheck: dependencies.arkCheck,
|
|
650
|
+
packageVersion: dependencies.cliVersion,
|
|
651
|
+
...(dependencies.checkResult ? { checkResult: dependencies.checkResult } : {}),
|
|
652
|
+
...(typeof dependencies.spawnArchitectureCheck === 'function'
|
|
653
|
+
? { checkResult: dependencies.spawnArchitectureCheck(root) }
|
|
654
|
+
: {}),
|
|
655
|
+
});
|
|
656
|
+
const outcome = upgradeOutcome({ applied: true, postUpgrade });
|
|
657
|
+
const verification = {
|
|
658
|
+
mode: postUpgrade.mode,
|
|
659
|
+
exitCode: postUpgrade.exitCode,
|
|
660
|
+
...(postUpgrade.stderr ? { stderr: postUpgrade.stderr } : {}),
|
|
661
|
+
};
|
|
636
662
|
const dualTruth = describePackageVersionDualTruth(root);
|
|
637
663
|
const skillDrift = buildSkillDriftSummary(applied);
|
|
638
664
|
const hostHonesty = buildHostSelectionHonesty(applied);
|
|
639
665
|
// FX05: post-upgrade verification block (advisory, notAScore).
|
|
640
666
|
const postUpgradeChecks = buildPostUpgradeChecks(root, {
|
|
641
667
|
cliVersion: dependencies.cliVersion,
|
|
642
|
-
verification
|
|
668
|
+
verification: {
|
|
669
|
+
...verification,
|
|
670
|
+
verdict: postUpgrade.verdict,
|
|
671
|
+
failing: postUpgrade.failing,
|
|
672
|
+
},
|
|
643
673
|
dualTruth,
|
|
644
674
|
});
|
|
675
|
+
const postUpgradePayload = {
|
|
676
|
+
verdict: postUpgrade.verdict,
|
|
677
|
+
failing: postUpgrade.failing,
|
|
678
|
+
behaviorChanges: postUpgrade.behaviorChanges,
|
|
679
|
+
};
|
|
645
680
|
if (args.json) {
|
|
646
681
|
console.log(
|
|
647
682
|
JSON.stringify(
|
|
648
683
|
{
|
|
649
684
|
...applied,
|
|
685
|
+
outcome: outcome.kind,
|
|
650
686
|
verification,
|
|
687
|
+
postUpgrade: postUpgradePayload,
|
|
651
688
|
skillDrift,
|
|
652
689
|
hostSelection: hostHonesty,
|
|
653
690
|
postUpgradeChecks,
|
|
@@ -673,6 +710,7 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
673
710
|
);
|
|
674
711
|
} else {
|
|
675
712
|
renderManagedUpgrade(applied, { skillDrift, hostSelection: hostHonesty });
|
|
713
|
+
for (const line of formatPostUpgradeHuman(postUpgrade)) console.log(line);
|
|
676
714
|
if (!args.strict) console.log('Architecture verification skipped (--no-strict).');
|
|
677
715
|
if (args.install === false || dualTruth.dualTruth) {
|
|
678
716
|
console.log(
|
|
@@ -690,5 +728,5 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
690
728
|
console.log(` [note] mcp: ${postUpgradeChecks.mcpNote}`);
|
|
691
729
|
}
|
|
692
730
|
}
|
|
693
|
-
return
|
|
731
|
+
return exitCodeFor(outcome);
|
|
694
732
|
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpgradeOutcome — one value for `upgrade --apply`.
|
|
3
|
+
*
|
|
4
|
+
* Sketch (D2). Callers see one object and one exit mapping. Gitignore
|
|
5
|
+
* detection, rule aggregation, and version-note lookup stay in here.
|
|
6
|
+
*
|
|
7
|
+
* classifyManagedAsset({ ..., gitignored }) →
|
|
8
|
+
* { state: 'absent-local', requiresConsent: false } when recorded + gitignored + missing
|
|
9
|
+
*
|
|
10
|
+
* runPostUpgradeVerification(root) →
|
|
11
|
+
* { verdict: 'green'|'red'|'skipped', failing: [{ ruleId, count, sample }], behaviorChanges: string[] }
|
|
12
|
+
*
|
|
13
|
+
* exitCodeFor(outcome) →
|
|
14
|
+
* 0 applied+green (also nothing-to-apply / verification skipped)
|
|
15
|
+
* 3 applied+red
|
|
16
|
+
* 2 blocked
|
|
17
|
+
* 1 error
|
|
18
|
+
*
|
|
19
|
+
* JSON fields on the existing upgrade payload are additive (`outcome`, `postUpgrade`,
|
|
20
|
+
* per-asset `reason` / `nextCommand`, state `absent-local`).
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
|
|
24
|
+
import { behaviorChangeLines } from './upgrade-behavior-changes.mjs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {'applied-green'|'applied-skipped'|'applied-red'|'blocked'|'error'|'nothing'|'preview'} UpgradeOutcomeKind
|
|
28
|
+
* @typedef {{ ruleId: string, count: number, sample: string }} FailingRule
|
|
29
|
+
* @typedef {{
|
|
30
|
+
* verdict: 'green'|'red'|'skipped',
|
|
31
|
+
* failing: FailingRule[],
|
|
32
|
+
* behaviorChanges: string[],
|
|
33
|
+
* mode: 'strict-merge'|'skipped',
|
|
34
|
+
* exitCode: number,
|
|
35
|
+
* stderr?: string,
|
|
36
|
+
* }} PostUpgradeVerification
|
|
37
|
+
* @typedef {{
|
|
38
|
+
* kind: UpgradeOutcomeKind,
|
|
39
|
+
* applied: boolean,
|
|
40
|
+
* blocked: boolean,
|
|
41
|
+
* postUpgrade: PostUpgradeVerification|null,
|
|
42
|
+
* error?: string,
|
|
43
|
+
* }} UpgradeOutcome
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
export const ABSENT_LOCAL_REASON =
|
|
47
|
+
'Recorded managed file is missing and gitignored (per-machine file). Recreated without consent.';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} state
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function blockedAssetReason(state) {
|
|
54
|
+
if (state === 'missing') {
|
|
55
|
+
return 'Recorded managed file is missing and is not gitignored. Apply will not recreate a deleted file without consent.';
|
|
56
|
+
}
|
|
57
|
+
if (state === 'conflicted') {
|
|
58
|
+
return 'Managed base and local edits both changed. Apply will not overwrite without consent.';
|
|
59
|
+
}
|
|
60
|
+
return 'Apply needs consent before changing this managed file.';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Paths git would ignore. Not a git repo, or git missing → empty set
|
|
65
|
+
* (fail closed toward consent). Global excludes are not consulted.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} root
|
|
68
|
+
* @param {string[]} relativePaths
|
|
69
|
+
* @returns {Set<string>}
|
|
70
|
+
*/
|
|
71
|
+
export function gitignoredPathSet(root, relativePaths) {
|
|
72
|
+
const paths = [...new Set(relativePaths.filter((entry) => typeof entry === 'string' && entry.length > 0))];
|
|
73
|
+
if (paths.length === 0) return new Set();
|
|
74
|
+
let result;
|
|
75
|
+
try {
|
|
76
|
+
result = spawnSync(
|
|
77
|
+
'git',
|
|
78
|
+
['-c', 'core.excludesFile=/dev/null', 'check-ignore', '-z', '--stdin'],
|
|
79
|
+
{
|
|
80
|
+
cwd: root,
|
|
81
|
+
input: `${paths.join('\0')}\0`,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
env: {
|
|
85
|
+
...process.env,
|
|
86
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
87
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
return new Set();
|
|
93
|
+
}
|
|
94
|
+
if (!result || result.error || (result.status !== 0 && result.status !== 1)) return new Set();
|
|
95
|
+
const ignored = new Set();
|
|
96
|
+
for (const entry of String(result.stdout ?? '').split('\0')) {
|
|
97
|
+
if (entry) ignored.add(entry);
|
|
98
|
+
}
|
|
99
|
+
return ignored;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {unknown} check
|
|
104
|
+
* @returns {FailingRule[]}
|
|
105
|
+
*/
|
|
106
|
+
export function aggregateFailingRules(check) {
|
|
107
|
+
const violations = Array.isArray(check?.violations) ? check.violations : [];
|
|
108
|
+
/** @type {Map<string, FailingRule>} */
|
|
109
|
+
const groups = new Map();
|
|
110
|
+
for (const violation of violations) {
|
|
111
|
+
if (!violation || typeof violation !== 'object') continue;
|
|
112
|
+
if (violation.failsStrict === false) continue;
|
|
113
|
+
const ruleId = typeof violation.ruleId === 'string' && violation.ruleId.trim()
|
|
114
|
+
? violation.ruleId.trim()
|
|
115
|
+
: 'UNKNOWN';
|
|
116
|
+
const existing = groups.get(ruleId) ?? { ruleId, count: 0, sample: '' };
|
|
117
|
+
existing.count += 1;
|
|
118
|
+
if (!existing.sample) existing.sample = sampleForViolation(violation, ruleId);
|
|
119
|
+
groups.set(ruleId, existing);
|
|
120
|
+
}
|
|
121
|
+
return [...groups.values()].sort(
|
|
122
|
+
(left, right) => right.count - left.count || left.ruleId.localeCompare(right.ruleId)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {Record<string, unknown>} violation
|
|
128
|
+
* @param {string} ruleId
|
|
129
|
+
*/
|
|
130
|
+
function sampleForViolation(violation, ruleId) {
|
|
131
|
+
const file = typeof violation.file === 'string'
|
|
132
|
+
? violation.file
|
|
133
|
+
: typeof violation.path === 'string'
|
|
134
|
+
? violation.path
|
|
135
|
+
: '';
|
|
136
|
+
const line = Number.isInteger(violation.line) ? `:${violation.line}` : '';
|
|
137
|
+
const where = file ? `${file}${line}` : '';
|
|
138
|
+
const message = typeof violation.message === 'string' ? violation.message.trim() : '';
|
|
139
|
+
const sample = [where, message].filter(Boolean).join(' — ') || ruleId;
|
|
140
|
+
return sample.length > 180 ? `${sample.slice(0, 177)}...` : sample;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} stdout
|
|
145
|
+
* @returns {Record<string, unknown>|null}
|
|
146
|
+
*/
|
|
147
|
+
export function parseCheckStdout(stdout) {
|
|
148
|
+
const text = typeof stdout === 'string' ? stdout.trim() : '';
|
|
149
|
+
if (!text) return null;
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(text);
|
|
152
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
153
|
+
} catch {
|
|
154
|
+
const start = text.indexOf('{');
|
|
155
|
+
const end = text.lastIndexOf('}');
|
|
156
|
+
if (start === -1 || end <= start) return null;
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
159
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} root
|
|
168
|
+
* @param {string} arkCheck
|
|
169
|
+
* @returns {{ exitCode: number, stdout: string, stderr: string }}
|
|
170
|
+
*/
|
|
171
|
+
function spawnArchitectureCheck(root, arkCheck) {
|
|
172
|
+
if (typeof arkCheck !== 'string' || !arkCheck) {
|
|
173
|
+
return { exitCode: 1, stdout: '', stderr: 'architecture check binary is missing' };
|
|
174
|
+
}
|
|
175
|
+
const result = spawnSync(
|
|
176
|
+
process.execPath,
|
|
177
|
+
[arkCheck, '--root', root, '--config', 'ark.config.json', '--strict-merge', '--json'],
|
|
178
|
+
{
|
|
179
|
+
cwd: root,
|
|
180
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
181
|
+
encoding: 'utf8',
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
return {
|
|
185
|
+
exitCode: result.status ?? 1,
|
|
186
|
+
stdout: result.stdout ?? '',
|
|
187
|
+
stderr: result.stderr ?? '',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Structured post-upgrade verification. Reuses check JSON (rule ids), and
|
|
193
|
+
* attaches behavior-change lines from the shipped per-version table.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} root
|
|
196
|
+
* @param {{
|
|
197
|
+
* strict?: boolean,
|
|
198
|
+
* arkCheck?: string,
|
|
199
|
+
* packageVersion?: string,
|
|
200
|
+
* checkResult?: { exitCode?: number, status?: number, stdout?: string, stderr?: string },
|
|
201
|
+
* }} [options]
|
|
202
|
+
* @returns {PostUpgradeVerification}
|
|
203
|
+
*/
|
|
204
|
+
export function runPostUpgradeVerification(root, options = {}) {
|
|
205
|
+
const behaviorChanges = behaviorChangeLines();
|
|
206
|
+
if (options.strict === false) {
|
|
207
|
+
return {
|
|
208
|
+
verdict: 'skipped',
|
|
209
|
+
failing: [],
|
|
210
|
+
behaviorChanges,
|
|
211
|
+
mode: 'skipped',
|
|
212
|
+
exitCode: 0,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const raw = options.checkResult ?? spawnArchitectureCheck(root, options.arkCheck);
|
|
216
|
+
const parsed = parseCheckStdout(raw.stdout ?? '');
|
|
217
|
+
const failing = aggregateFailingRules(parsed);
|
|
218
|
+
const exitCode = raw.exitCode ?? raw.status ?? 1;
|
|
219
|
+
const red = exitCode !== 0 || parsed?.ok === false || parsed?.valid === false;
|
|
220
|
+
if (red && failing.length === 0) {
|
|
221
|
+
const sampleSource = String(raw.stderr || 'architecture check failed').trim() || 'architecture check failed';
|
|
222
|
+
failing.push({
|
|
223
|
+
ruleId: 'ARCHITECTURE_CHECK',
|
|
224
|
+
count: 1,
|
|
225
|
+
sample: sampleSource.length > 180 ? `${sampleSource.slice(0, 177)}...` : sampleSource,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const stderr = typeof raw.stderr === 'string' && raw.stderr.trim() ? raw.stderr.trim() : undefined;
|
|
229
|
+
return {
|
|
230
|
+
verdict: red ? 'red' : 'green',
|
|
231
|
+
failing: red ? failing : [],
|
|
232
|
+
behaviorChanges,
|
|
233
|
+
mode: 'strict-merge',
|
|
234
|
+
exitCode: red ? (exitCode || 1) : 0,
|
|
235
|
+
...(stderr ? { stderr } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {{
|
|
241
|
+
* applied?: boolean,
|
|
242
|
+
* blocked?: boolean,
|
|
243
|
+
* nothingToApply?: boolean,
|
|
244
|
+
* postUpgrade?: PostUpgradeVerification|null,
|
|
245
|
+
* error?: unknown,
|
|
246
|
+
* }} input
|
|
247
|
+
* @returns {UpgradeOutcome}
|
|
248
|
+
*/
|
|
249
|
+
export function upgradeOutcome(input) {
|
|
250
|
+
if (input.error != null && input.error !== false) {
|
|
251
|
+
const message = input.error instanceof Error ? input.error.message : String(input.error);
|
|
252
|
+
return { kind: 'error', applied: false, blocked: false, postUpgrade: null, error: message };
|
|
253
|
+
}
|
|
254
|
+
if (input.blocked) {
|
|
255
|
+
return { kind: 'blocked', applied: false, blocked: true, postUpgrade: null };
|
|
256
|
+
}
|
|
257
|
+
if (input.nothingToApply && !input.applied) {
|
|
258
|
+
return { kind: 'nothing', applied: false, blocked: false, postUpgrade: null };
|
|
259
|
+
}
|
|
260
|
+
const postUpgrade = input.postUpgrade ?? null;
|
|
261
|
+
if (postUpgrade?.verdict === 'red') {
|
|
262
|
+
return { kind: 'applied-red', applied: true, blocked: false, postUpgrade };
|
|
263
|
+
}
|
|
264
|
+
if (postUpgrade?.verdict === 'skipped') {
|
|
265
|
+
return { kind: 'applied-skipped', applied: Boolean(input.applied), blocked: false, postUpgrade };
|
|
266
|
+
}
|
|
267
|
+
return { kind: 'applied-green', applied: Boolean(input.applied), blocked: false, postUpgrade };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {UpgradeOutcome|null|undefined} outcome
|
|
272
|
+
* @returns {0|1|2|3}
|
|
273
|
+
*/
|
|
274
|
+
export function exitCodeFor(outcome) {
|
|
275
|
+
switch (outcome?.kind) {
|
|
276
|
+
case 'applied-green':
|
|
277
|
+
case 'applied-skipped':
|
|
278
|
+
case 'nothing':
|
|
279
|
+
case 'preview':
|
|
280
|
+
return 0;
|
|
281
|
+
case 'applied-red':
|
|
282
|
+
return 3;
|
|
283
|
+
case 'blocked':
|
|
284
|
+
return 2;
|
|
285
|
+
case 'error':
|
|
286
|
+
return 1;
|
|
287
|
+
default:
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {{ mode?: string, exitCode?: number, verdict?: string }} [verification]
|
|
294
|
+
* @returns {boolean|null}
|
|
295
|
+
*/
|
|
296
|
+
export function architectureVerificationOk(verification) {
|
|
297
|
+
if (!verification || verification.mode === 'skipped' || verification.verdict === 'skipped') return null;
|
|
298
|
+
if (verification.verdict === 'green') return true;
|
|
299
|
+
if (verification.verdict === 'red') return false;
|
|
300
|
+
if (verification.exitCode === 0) return true;
|
|
301
|
+
if (typeof verification.exitCode === 'number') return false;
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {{ mode?: string, exitCode?: number, verdict?: string, failing?: FailingRule[] }} [verification]
|
|
307
|
+
* @returns {string}
|
|
308
|
+
*/
|
|
309
|
+
export function architectureVerificationDetail(verification) {
|
|
310
|
+
if (!verification || verification.mode === 'skipped' || verification.verdict === 'skipped') {
|
|
311
|
+
return 'Strict architecture verification was skipped (--no-strict).';
|
|
312
|
+
}
|
|
313
|
+
const code = verification.exitCode;
|
|
314
|
+
if (architectureVerificationOk(verification) === true) {
|
|
315
|
+
return 'Strict-merge architecture verification passed.';
|
|
316
|
+
}
|
|
317
|
+
const failing = Array.isArray(verification.failing) ? verification.failing : [];
|
|
318
|
+
const rules = failing.map((row) => `${row.ruleId} ×${row.count}`).join(', ');
|
|
319
|
+
const head = `Architecture verification exit ${code ?? 'unknown'}.`;
|
|
320
|
+
if (!rules) return head;
|
|
321
|
+
return `${head} Failing rules: ${rules}. Read behaviorChanges before editing code.`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {{ assets?: Array<{ blocked?: boolean, path?: string, state?: string, reason?: string, nextCommand?: string }>, blocked?: boolean, nextCommand?: string }} plan
|
|
326
|
+
* @param {{ refused?: boolean }} [options]
|
|
327
|
+
* @returns {string[]}
|
|
328
|
+
*/
|
|
329
|
+
export function formatBlockedAssets(plan, options = {}) {
|
|
330
|
+
const blocked = (plan?.assets ?? []).filter((asset) => asset?.blocked);
|
|
331
|
+
if (blocked.length === 0) return [];
|
|
332
|
+
const lines = [options.refused ? 'Apply refused.' : 'Blocked until you consent:'];
|
|
333
|
+
for (const asset of blocked) {
|
|
334
|
+
const reason = asset.reason || blockedAssetReason(asset.state ?? '');
|
|
335
|
+
lines.push(` ${asset.path} — ${reason}`);
|
|
336
|
+
}
|
|
337
|
+
const next = blocked.find((asset) => asset.nextCommand)?.nextCommand ?? plan?.nextCommand;
|
|
338
|
+
if (next) lines.push(`Next: ${next}`);
|
|
339
|
+
return lines;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {PostUpgradeVerification|null|undefined} postUpgrade
|
|
344
|
+
* @returns {string[]}
|
|
345
|
+
*/
|
|
346
|
+
export function formatPostUpgradeHuman(postUpgrade) {
|
|
347
|
+
if (!postUpgrade) return [];
|
|
348
|
+
const lines = [`Post-upgrade verification: ${postUpgrade.verdict}.`];
|
|
349
|
+
if (postUpgrade.verdict !== 'red') return lines;
|
|
350
|
+
lines.push('Failing rules:');
|
|
351
|
+
const failing = postUpgrade.failing ?? [];
|
|
352
|
+
if (failing.length === 0) lines.push(' (check returned red without a rule id)');
|
|
353
|
+
for (const row of failing) {
|
|
354
|
+
lines.push(` ${row.ruleId} ×${row.count} — ${row.sample}`);
|
|
355
|
+
}
|
|
356
|
+
lines.push('Read behaviorChanges before editing code.');
|
|
357
|
+
for (const note of postUpgrade.behaviorChanges ?? []) lines.push(` ${note}`);
|
|
358
|
+
return lines;
|
|
359
|
+
}
|
package/bin/lib/violations.mjs
CHANGED
|
@@ -97,6 +97,115 @@ export function printWarning(warning) {
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** Advisory the engine emits once per shared-root → slice edge. Humans see a group. */
|
|
101
|
+
export const SHARED_IMPORTS_SLICE_RULE_ID = 'SHARED_IMPORTS_SLICE';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Where the full per-edge list lives. Human check output names this on every
|
|
105
|
+
* grouped line. `--json` is the machine list; `--doctor` prints every edge.
|
|
106
|
+
*/
|
|
107
|
+
export const SHARED_IMPORTS_SLICE_FULL_LIST =
|
|
108
|
+
'Full list: ark-check --json or ark-check --doctor';
|
|
109
|
+
|
|
110
|
+
const SHARED_IMPORTS_SLICE_EXAMPLE_LIMIT = 3;
|
|
111
|
+
|
|
112
|
+
function formatAdvisoryCount(count) {
|
|
113
|
+
return String(count).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function layerLabel(value) {
|
|
117
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '?';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Group key: one fact per rule and layer edge. */
|
|
121
|
+
export function sharedImportsSliceGroupKey(warning) {
|
|
122
|
+
return [
|
|
123
|
+
typeof warning?.ruleId === 'string' ? warning.ruleId : SHARED_IMPORTS_SLICE_RULE_ID,
|
|
124
|
+
layerLabel(warning?.fromLayer),
|
|
125
|
+
layerLabel(warning?.toLayer),
|
|
126
|
+
].join('\0');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatSharedImportsSliceExample(warning) {
|
|
130
|
+
const file = typeof warning?.file === 'string' && warning.file.trim() ? warning.file.trim() : 'unknown';
|
|
131
|
+
const line = Number.isInteger(warning?.line) && warning.line > 0 ? warning.line : 1;
|
|
132
|
+
const slice =
|
|
133
|
+
typeof warning?.toSlice === 'string' && warning.toSlice.trim() ? warning.toSlice.trim() : 'slice';
|
|
134
|
+
const target =
|
|
135
|
+
typeof warning?.target === 'string' && warning.target.trim() ? ` (${warning.target.trim()})` : '';
|
|
136
|
+
return `${file}:${line} → ${slice}${target}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One human line for a (ruleId, fromLayer → toLayer) group. Count plus top 3 edges. */
|
|
140
|
+
export function formatSharedImportsSliceSummary(group) {
|
|
141
|
+
const sample = group[0] ?? {};
|
|
142
|
+
const ruleId =
|
|
143
|
+
typeof sample.ruleId === 'string' ? sample.ruleId : SHARED_IMPORTS_SLICE_RULE_ID;
|
|
144
|
+
const count = group.length;
|
|
145
|
+
const noun = count === 1 ? 'bridge' : 'bridges';
|
|
146
|
+
const examples = group
|
|
147
|
+
.slice(0, SHARED_IMPORTS_SLICE_EXAMPLE_LIMIT)
|
|
148
|
+
.map(formatSharedImportsSliceExample)
|
|
149
|
+
.join('; ');
|
|
150
|
+
return (
|
|
151
|
+
`warning ${ruleId} ${layerLabel(sample.fromLayer)} → ${layerLabel(sample.toLayer)}: ` +
|
|
152
|
+
`${formatAdvisoryCount(count)} shared-root → slice ${noun} (e.g. ${examples}). ` +
|
|
153
|
+
`The wall is direct-only. ${SHARED_IMPORTS_SLICE_FULL_LIST}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Human check lines. SHARED_IMPORTS_SLICE collapses to one line per group.
|
|
159
|
+
* Every other warning stays one line. Does not mutate `warnings` — `--json`
|
|
160
|
+
* and doctor still walk the per-edge array.
|
|
161
|
+
*/
|
|
162
|
+
export function humanWarningLines(warnings) {
|
|
163
|
+
const groups = new Map();
|
|
164
|
+
const slots = [];
|
|
165
|
+
for (const warning of warnings ?? []) {
|
|
166
|
+
if (warning?.ruleId !== SHARED_IMPORTS_SLICE_RULE_ID) {
|
|
167
|
+
slots.push({ kind: 'line', line: formatWarningLine(warning) });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const key = sharedImportsSliceGroupKey(warning);
|
|
171
|
+
let group = groups.get(key);
|
|
172
|
+
if (!group) {
|
|
173
|
+
group = [];
|
|
174
|
+
groups.set(key, group);
|
|
175
|
+
slots.push({ kind: 'group', key });
|
|
176
|
+
}
|
|
177
|
+
group.push(warning);
|
|
178
|
+
}
|
|
179
|
+
return slots.map((slot) =>
|
|
180
|
+
slot.kind === 'line' ? slot.line : formatSharedImportsSliceSummary(groups.get(slot.key))
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function printColoredWarningLine(line) {
|
|
185
|
+
const painted = line.startsWith('warning ')
|
|
186
|
+
? `${color.yellow('warning')}${line.slice('warning'.length)}`
|
|
187
|
+
: line;
|
|
188
|
+
console.error(painted);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** ark-check human renderer. Grouping stops here; the warnings array is unchanged. */
|
|
192
|
+
export function printHumanWarnings(warnings) {
|
|
193
|
+
for (const line of humanWarningLines(warnings)) {
|
|
194
|
+
printColoredWarningLine(line);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Doctor human list: every shared-root → slice edge, not the grouped summary. */
|
|
199
|
+
export function printSharedImportsSliceBridgeList(warnings) {
|
|
200
|
+
const lines = (warnings ?? [])
|
|
201
|
+
.filter((warning) => warning?.ruleId === SHARED_IMPORTS_SLICE_RULE_ID)
|
|
202
|
+
.map((warning) => formatWarningLine(warning));
|
|
203
|
+
if (lines.length === 0) return;
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(`Shared-root → slice bridges (${formatAdvisoryCount(lines.length)}):`);
|
|
206
|
+
for (const line of lines) console.log(line);
|
|
207
|
+
}
|
|
208
|
+
|
|
100
209
|
/** Non-blocking type-only placement debt: exit stays 0; do not paint ✖. */
|
|
101
210
|
export function isNonBlockingPlacementDebt(violation) {
|
|
102
211
|
return violation?.failsStrict === false;
|
|
@@ -49,6 +49,8 @@ type ArkConfigLayer = {
|
|
|
49
49
|
/** Alias of reserved — empty pattern matches are allowed. */
|
|
50
50
|
allowEmpty?: boolean;
|
|
51
51
|
};
|
|
52
|
+
/** How a starred `sliceFolders` prefix is named. Absent equals `path`. */
|
|
53
|
+
type ArkConfigSliceIdentity = 'path' | 'stars';
|
|
52
54
|
type ArkConfigRule = {
|
|
53
55
|
from: string;
|
|
54
56
|
to: string;
|
|
@@ -56,6 +58,11 @@ type ArkConfigRule = {
|
|
|
56
58
|
message?: string;
|
|
57
59
|
peerIsolation?: boolean;
|
|
58
60
|
sliceFolders?: string[];
|
|
61
|
+
/**
|
|
62
|
+
* How a starred `sliceFolders` prefix is named.
|
|
63
|
+
* Absent and `path` keep today's ids. `stars` is the last literal plus the star bindings.
|
|
64
|
+
*/
|
|
65
|
+
sliceIdentity?: ArkConfigSliceIdentity;
|
|
59
66
|
/** Roots the repo declares shared on purpose — evidence, not unclassifiable. */
|
|
60
67
|
sharedRoots?: string[];
|
|
61
68
|
/** Directed slice→slice edges the repo declares on purpose. */
|