arkgate 4.8.14 → 4.8.16
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 -0
- package/README.md +17 -7
- package/bin/ark-check-runtime.mjs +17 -49
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +142 -11
- package/bin/ark.mjs +24 -33
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-skills-package.mjs +179 -1
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +43 -4
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +6 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/config-contract.mjs +65 -2
- package/bin/lib/diagnostic-catalog.mjs +5 -1
- package/bin/lib/doctor-advisories.mjs +120 -10
- package/bin/lib/doctor-green-cite.mjs +139 -0
- package/bin/lib/doctor-human.mjs +115 -67
- package/bin/lib/doctor-next-actions.mjs +23 -0
- package/bin/lib/doctor-plan.mjs +35 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/field-install.mjs +47 -7
- package/bin/lib/first-run-help.mjs +9 -1
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +8 -1
- package/bin/lib/improvement-compass-map.mjs +5 -1
- package/bin/lib/invariant-coverage.mjs +121 -0
- package/bin/lib/invariant-tests-path.mjs +212 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +56 -0
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/package-manager.mjs +8 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +15 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/prototype-shortcuts.mjs +224 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +15 -0
- package/bin/lib/resolved-candidate-facts.mjs +83 -66
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +24 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/violations.mjs +13 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-VD0qcubY.d.ts} +21 -1
- package/dist/{diagnosticCatalog-DVx_2RmF.d.ts → diagnosticCatalog-KWvGLI1U.d.ts} +24 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +34 -34
- package/dist/index.d.ts +71 -10
- package/dist/index.js +34 -34
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-tGhZUiGX.d.ts → types-BSzRy2X1.d.ts} +1 -1
- package/dist/{types-Djbs3KjE.d.ts → types-D5GT5ZT8.d.ts} +3 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +8 -4
- package/docs/ai-gates.md +10 -3
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +66 -10
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +48 -4
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +7 -5
- package/docs/use.md +10 -3
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +24 -3
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +97 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +12 -3
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -2
- package/templates/agent-skills/ark-explain/SKILL.md +6 -3
- package/templates/agent-skills/ark-explore/SKILL.md +33 -1
- package/templates/agent-skills/ark-place/SKILL.md +42 -3
- package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +97 -1
- package/templates/skills/ark-autopilot.md +12 -3
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-explain.md +6 -3
- package/templates/skills/ark-explore.md +33 -1
- package/templates/skills/ark-place.md +42 -3
- package/templates/skills/ark-runtime.md +4 -2
- package/templates/skills/ark-upgrade.md +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden rule: a doctor green / healthy claim must cite a file, config key,
|
|
3
|
+
* or test. Uncited greens are dishonest — demote or omit.
|
|
4
|
+
*
|
|
5
|
+
* Tooling only. Reuses existing doctor view fields (IT01 / ADR / CI
|
|
6
|
+
* residual shape). No new schema, RPC, or skill name.
|
|
7
|
+
*/
|
|
8
|
+
import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
|
|
9
|
+
import { CI_MERGE_BOUNDARY_REL } from './ci-merge-boundary.mjs';
|
|
10
|
+
|
|
11
|
+
export const HEALTHY_CLAIM = 'Healthy — nothing to do.';
|
|
12
|
+
export const HEALTHY_KEEP = ' Keep write path + CI.';
|
|
13
|
+
export const UNCITE_SUFFIX =
|
|
14
|
+
'not green until a file, config key, or test is named';
|
|
15
|
+
|
|
16
|
+
const CONFIG_KEY_DOT = /^[A-Za-z_][\w]*\.[A-Za-z_][\w.]*$/;
|
|
17
|
+
const CONFIG_KEY_BARE = new Set([
|
|
18
|
+
'include',
|
|
19
|
+
'exclude',
|
|
20
|
+
'layers',
|
|
21
|
+
'rules',
|
|
22
|
+
'arkRules',
|
|
23
|
+
'arkRun',
|
|
24
|
+
'arkOrder',
|
|
25
|
+
'coverage',
|
|
26
|
+
'stewards',
|
|
27
|
+
'schemaVersion',
|
|
28
|
+
]);
|
|
29
|
+
const FILE_LIKE =
|
|
30
|
+
/(?:^|\/)(?:\.[A-Za-z][\w.-]*|[A-Za-z][\w.-]*\.(?:json|ya?ml|md|ts|mjs|js|cjs))$|\/|[.](?:json|ya?ml|md|ts|mjs|js|cjs)$/;
|
|
31
|
+
const TEST_LIKE = /(?:^|\/)tests?\/|(?:\.|\b)test\.(?:ts|mjs|js)\b/i;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True when a stranger can open this in about a minute.
|
|
35
|
+
* Generic words ("CI", "gates") are not cites.
|
|
36
|
+
*/
|
|
37
|
+
export function isConcreteCite(value) {
|
|
38
|
+
if (typeof value !== 'string') return false;
|
|
39
|
+
const cite = value.trim();
|
|
40
|
+
if (cite.length < 3 || cite.length > 240) return false;
|
|
41
|
+
if (/\s/.test(cite)) return false;
|
|
42
|
+
if (cite === REQUIRED_GATE_WORKFLOW) return false;
|
|
43
|
+
return (
|
|
44
|
+
FILE_LIKE.test(cite) ||
|
|
45
|
+
TEST_LIKE.test(cite) ||
|
|
46
|
+
CONFIG_KEY_DOT.test(cite) ||
|
|
47
|
+
CONFIG_KEY_BARE.has(cite)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function uniqueConcreteCites(cites) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const raw of Array.isArray(cites) ? cites : [cites]) {
|
|
54
|
+
if (!isConcreteCite(raw)) continue;
|
|
55
|
+
const cite = String(raw).trim();
|
|
56
|
+
if (!out.includes(cite)) out.push(cite);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @returns {{ mark: 'ok', text: string } | { mark: 'warn', text: string }}
|
|
63
|
+
*/
|
|
64
|
+
export function citeOrDemoteGreen(claim, cites) {
|
|
65
|
+
const named = typeof claim === 'string' && claim.trim() ? claim.trim() : 'This claim';
|
|
66
|
+
const list = uniqueConcreteCites(cites);
|
|
67
|
+
if (list.length > 0) {
|
|
68
|
+
return { mark: 'ok', text: `${named} (${list.join(' · ')})` };
|
|
69
|
+
}
|
|
70
|
+
return { mark: 'warn', text: `${named} — ${UNCITE_SUFFIX}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function citedGreen(line, marks, claim, cites) {
|
|
74
|
+
const row = citeOrDemoteGreen(claim, cites);
|
|
75
|
+
line(row.mark === 'ok' ? marks.ok : marks.warn, row.text);
|
|
76
|
+
return row;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Backing artifacts already on the doctor view — no second scan. */
|
|
80
|
+
export function healthyCitesFromView(view) {
|
|
81
|
+
const cites = ['ark.config.json'];
|
|
82
|
+
if (view?.ciMergeBoundary) cites.push(CI_MERGE_BOUNDARY_REL);
|
|
83
|
+
const workflow =
|
|
84
|
+
view?.ciMergeBoundary?.ci?.workflowFile || view?.ciNotFailClosed?.workflowFile;
|
|
85
|
+
if (workflow) cites.push(workflow);
|
|
86
|
+
return uniqueConcreteCites(cites);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function printHealthyHeadline(view, color) {
|
|
90
|
+
const row = citeOrDemoteGreen(HEALTHY_CLAIM, healthyCitesFromView(view));
|
|
91
|
+
if (row.mark === 'ok') {
|
|
92
|
+
console.log(color.green(`✔ ${row.text}`));
|
|
93
|
+
console.log(color.dim(HEALTHY_KEEP));
|
|
94
|
+
return row;
|
|
95
|
+
}
|
|
96
|
+
console.log(color.yellow(`! ${row.text}`));
|
|
97
|
+
return row;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function displayedMissingGates(gatesMissing, view) {
|
|
101
|
+
const list = Array.isArray(gatesMissing) ? gatesMissing : [];
|
|
102
|
+
const hideGlob = Boolean(
|
|
103
|
+
view?.ciNotFailClosed?.workflowFile ||
|
|
104
|
+
view?.ciNotFailClosed?.error === 'ci-not-fail-closed' ||
|
|
105
|
+
view?.ciMergeBoundary?.ci?.workflowPresent
|
|
106
|
+
);
|
|
107
|
+
return hideGlob ? list.filter((item) => item !== REQUIRED_GATE_WORKFLOW) : list;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function ciNotFailClosedNotice(view) {
|
|
111
|
+
const file = view?.ciNotFailClosed?.workflowFile;
|
|
112
|
+
if (!file) return null;
|
|
113
|
+
return `CI not fail-closed: ${file} — remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function ciMergeGreenCites(view) {
|
|
117
|
+
const cites = [];
|
|
118
|
+
if (view?.ciMergeBoundary) cites.push(CI_MERGE_BOUNDARY_REL);
|
|
119
|
+
const workflow = view?.ciMergeBoundary?.ci?.workflowFile || view?.ciNotFailClosed?.workflowFile;
|
|
120
|
+
if (workflow) cites.push(workflow);
|
|
121
|
+
return uniqueConcreteCites(cites);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function foundGateCites(view) {
|
|
125
|
+
const cites = ['AGENTS.md'];
|
|
126
|
+
if (view?.ciMergeBoundary) cites.push(CI_MERGE_BOUNDARY_REL);
|
|
127
|
+
const workflow = view?.ciMergeBoundary?.ci?.workflowFile;
|
|
128
|
+
if (workflow) cites.push(workflow);
|
|
129
|
+
return uniqueConcreteCites(cites);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function writePathGreenCites(writePath) {
|
|
133
|
+
return uniqueConcreteCites([
|
|
134
|
+
...(Array.isArray(writePath?.capabilityEvidence?.['hard-write'])
|
|
135
|
+
? writePath.capabilityEvidence['hard-write']
|
|
136
|
+
: []),
|
|
137
|
+
...(Array.isArray(writePath?.evidence) ? writePath.evidence : []),
|
|
138
|
+
]);
|
|
139
|
+
}
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -13,23 +13,16 @@ import { enforcementDoctorLines } from './enforcement-state.mjs';
|
|
|
13
13
|
import { analysisIncompleteStatement } from './analysis-completeness.mjs';
|
|
14
14
|
import { skillGapsForActiveHost, detectCodexHomeGap, codexConcernIsActive } from './agent-gates.mjs';
|
|
15
15
|
import { agentHomeConcernIsActive } from './agent-homes.mjs';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function ciNotFailClosedNotice(view) {
|
|
29
|
-
const file = view?.ciNotFailClosed?.workflowFile;
|
|
30
|
-
if (!file) return null;
|
|
31
|
-
return `CI not fail-closed: ${file} — remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only`;
|
|
32
|
-
}
|
|
16
|
+
import { layerGuidanceLine } from './layer-description.mjs';
|
|
17
|
+
import {
|
|
18
|
+
citedGreen,
|
|
19
|
+
ciMergeGreenCites,
|
|
20
|
+
ciNotFailClosedNotice,
|
|
21
|
+
displayedMissingGates,
|
|
22
|
+
foundGateCites,
|
|
23
|
+
printHealthyHeadline,
|
|
24
|
+
writePathGreenCites,
|
|
25
|
+
} from './doctor-green-cite.mjs';
|
|
33
26
|
|
|
34
27
|
function lineWith(ok, warn, bad, color) {
|
|
35
28
|
return (mark, text) => console.log(` ${mark} ${text}`);
|
|
@@ -96,14 +89,13 @@ export function printDoctorCompactHuman(view) {
|
|
|
96
89
|
: 'import rules check out. Keep host + CI.',
|
|
97
90
|
};
|
|
98
91
|
const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
92
|
+
const modeClaim = `${modeTitle} — ${
|
|
93
|
+
designFitness.designWeak
|
|
94
|
+
? 'import rules check out; leftover design work remains.'
|
|
95
|
+
: modeHelp[mode]
|
|
96
|
+
}`;
|
|
97
|
+
if (modeMark === ok) citedGreen(line, { ok, warn }, modeClaim, ['ark.config.json']);
|
|
98
|
+
else line(modeMark, modeClaim);
|
|
107
99
|
if (emptyScope) {
|
|
108
100
|
line(
|
|
109
101
|
bad,
|
|
@@ -113,17 +105,18 @@ export function printDoctorCompactHuman(view) {
|
|
|
113
105
|
|
|
114
106
|
console.log('');
|
|
115
107
|
if (ciMergeBoundary?.ci?.state) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
const mergeClaim = `CI merge: ${ciMergeBoundary.ci.state}`;
|
|
109
|
+
if (ciMergeBoundary.ci.state === 'required') {
|
|
110
|
+
citedGreen(line, { ok, warn }, mergeClaim, ciMergeGreenCites(view));
|
|
111
|
+
} else {
|
|
112
|
+
line(warn, mergeClaim);
|
|
113
|
+
}
|
|
120
114
|
}
|
|
121
115
|
if (adopted === 'advisory-only-acked') {
|
|
122
116
|
line(warn, 'Adoption: advisory-only ack — not a required GitHub status.');
|
|
123
117
|
}
|
|
124
118
|
if (isDoctorHealthyNothingToDo(designFitness, uniqueActions, adopted)) {
|
|
125
|
-
|
|
126
|
-
console.log(color.dim(' Keep write path + CI.'));
|
|
119
|
+
printHealthyHeadline(view, color);
|
|
127
120
|
} else {
|
|
128
121
|
console.log(color.bold('Primary next action'));
|
|
129
122
|
console.log(` 1. ${uniqueActions[0]}`);
|
|
@@ -131,21 +124,20 @@ export function printDoctorCompactHuman(view) {
|
|
|
131
124
|
|
|
132
125
|
console.log('');
|
|
133
126
|
console.log(color.bold('Coverage'));
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
? ok
|
|
139
|
-
: warn;
|
|
140
|
-
line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
|
|
127
|
+
const govClaim = `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`;
|
|
128
|
+
if (emptyScope || cov.governed.percent < 50) line(bad, govClaim);
|
|
129
|
+
else if (cov.governed.percent >= 80) citedGreen(line, { ok, warn }, govClaim, ['ark.config.json', 'include', 'layers']);
|
|
130
|
+
else line(warn, govClaim);
|
|
141
131
|
for (const row of cov.layers ?? []) {
|
|
142
|
-
|
|
132
|
+
const guidance = layerGuidanceLine(row);
|
|
133
|
+
if (guidance) line(' ', `${row.name} — ${guidance}`);
|
|
143
134
|
}
|
|
144
135
|
|
|
145
136
|
const hostRed =
|
|
146
137
|
listedMissing.length > 0 ||
|
|
147
138
|
Boolean(writePath.gap) ||
|
|
148
139
|
writePathHonesty?.softWriteHost === true ||
|
|
140
|
+
writePathHonesty?.nativeFailClosed === false ||
|
|
149
141
|
Boolean(skippableCi);
|
|
150
142
|
if (hostRed) {
|
|
151
143
|
console.log('');
|
|
@@ -153,7 +145,9 @@ export function printDoctorCompactHuman(view) {
|
|
|
153
145
|
if (writePath.activeHost) line(' ', `Active host: ${writePath.activeHost}`);
|
|
154
146
|
if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
|
|
155
147
|
if (skippableCi) line(warn, skippableCi);
|
|
156
|
-
else if (
|
|
148
|
+
else if (writePathHonesty?.nativeFailClosed === false) {
|
|
149
|
+
line(warn, writePathHonesty.message || 'Write hook is fail-open — if the checker cannot run, the write still lands.');
|
|
150
|
+
} else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
|
|
157
151
|
line(warn, 'Local writes are advisory; required CI is the merge boundary.');
|
|
158
152
|
}
|
|
159
153
|
}
|
|
@@ -165,7 +159,15 @@ export function printDoctorCompactHuman(view) {
|
|
|
165
159
|
line(warn, nudge.ask);
|
|
166
160
|
}
|
|
167
161
|
|
|
168
|
-
printCompactExtraDoctorLines(
|
|
162
|
+
printCompactExtraDoctorLines(
|
|
163
|
+
{
|
|
164
|
+
...doctorAdvisories,
|
|
165
|
+
layerOwners: view.layerOwners,
|
|
166
|
+
adrPresence: view.adrPresence,
|
|
167
|
+
statesTransitions: view.statesTransitions, statusTransitionCatalog: view.statusTransitionCatalog, noDomainFrontend: view.noDomainFrontend, invariantTestsPath: view.invariantTestsPath, invariantCoverageRoots: view.invariantCoverageRoots,
|
|
168
|
+
},
|
|
169
|
+
{ line, warn }
|
|
170
|
+
);
|
|
169
171
|
|
|
170
172
|
if (violations.length === 0) {
|
|
171
173
|
if (!analysisComplete) {
|
|
@@ -248,12 +250,17 @@ export function printDoctorDetailsHuman(view) {
|
|
|
248
250
|
`Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
|
|
249
251
|
);
|
|
250
252
|
}
|
|
251
|
-
if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0)
|
|
252
|
-
|
|
253
|
+
if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) {
|
|
254
|
+
citedGreen(line, { ok, warn }, 'Every layer classifies files; no empty layers', [
|
|
255
|
+
'ark.config.json',
|
|
256
|
+
'layers',
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
const captioned = (cov.layers ?? []).filter((row) => layerGuidanceLine(row));
|
|
253
260
|
if (captioned.length > 0) {
|
|
254
261
|
console.log('');
|
|
255
262
|
console.log(color.bold('Layers'));
|
|
256
|
-
for (const row of captioned) line(' ', `${row.name} — ${row
|
|
263
|
+
for (const row of captioned) line(' ', `${row.name} — ${layerGuidanceLine(row)}`);
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
if (packageVersionTruth?.dualTruth) {
|
|
@@ -275,7 +282,8 @@ export function printDoctorDetailsHuman(view) {
|
|
|
275
282
|
console.log('');
|
|
276
283
|
console.log(color.bold('Design fitness'));
|
|
277
284
|
if (designSmells.length === 0) {
|
|
278
|
-
|
|
285
|
+
if (analysisComplete) citedGreen(line, { ok, warn }, designFitness.label, ['ark.config.json']);
|
|
286
|
+
else line(warn, designFitness.label);
|
|
279
287
|
} else {
|
|
280
288
|
line(designFitness.designWeak ? warn : warn, designFitness.label);
|
|
281
289
|
for (const smell of designSmells.slice(0, 5)) {
|
|
@@ -342,15 +350,16 @@ export function printDoctorDetailsHuman(view) {
|
|
|
342
350
|
} else if (designFitness.designWeak) {
|
|
343
351
|
line(warn, `None on checked imports — import rules match the config; leftover design work remains (${modeTitle}). Not healthy finished.`);
|
|
344
352
|
} else {
|
|
345
|
-
line
|
|
353
|
+
citedGreen(line, { ok, warn }, 'None — the code matches the contract on checked edges', [
|
|
354
|
+
'ark.config.json',
|
|
355
|
+
]);
|
|
346
356
|
}
|
|
347
357
|
} else {
|
|
348
358
|
const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
|
|
349
359
|
const supNote = suppressed > 0 ? `, ${suppressed} frozen` : '';
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
);
|
|
360
|
+
const violClaim = `${violations.length} total${typeNote}${supNote}${activeCount > 0 ? ` — ${activeCount} NOT baselined` : ''}`;
|
|
361
|
+
if (activeCount > 0) line(warn, violClaim);
|
|
362
|
+
else citedGreen(line, { ok, warn }, violClaim, ['.ark-baseline.json']);
|
|
354
363
|
for (const edge of summary.edges.slice(0, 3)) line(' ', color.dim(`${edge.count} ${edge.edge}`));
|
|
355
364
|
if (summary.concentrated && typeof summary.dominant === 'string' && summary.dominant.includes(' → ')) {
|
|
356
365
|
line(warn, color.dim(`${Math.round(summary.dominantShare * 100)}% on one edge (${summary.dominant}) — likely a contract fix, not debt`));
|
|
@@ -374,7 +383,17 @@ export function printDoctorDetailsHuman(view) {
|
|
|
374
383
|
: bad;
|
|
375
384
|
line(' ', `Active host: ${writePath.activeHost}`);
|
|
376
385
|
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
377
|
-
|
|
386
|
+
const modeLine = `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`;
|
|
387
|
+
if (wpMark === ok) {
|
|
388
|
+
citedGreen(
|
|
389
|
+
line,
|
|
390
|
+
{ ok, warn },
|
|
391
|
+
modeLine,
|
|
392
|
+
writePathGreenCites(writePath)
|
|
393
|
+
);
|
|
394
|
+
} else {
|
|
395
|
+
line(wpMark, modeLine);
|
|
396
|
+
}
|
|
378
397
|
if (writePathHonesty.message) line(warn, writePathHonesty.message);
|
|
379
398
|
if (writePath.sessionNote) {
|
|
380
399
|
line(warn, writePath.sessionNote);
|
|
@@ -384,14 +403,21 @@ export function printDoctorDetailsHuman(view) {
|
|
|
384
403
|
const supportCaps = writePath.support?.capabilities || {};
|
|
385
404
|
const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
|
|
386
405
|
const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
:
|
|
406
|
+
if (repairReinjection) {
|
|
407
|
+
citedGreen(
|
|
408
|
+
line,
|
|
409
|
+
{ ok, warn },
|
|
410
|
+
'Repair: envelope + reinjection guaranteed on hard path when installed + trusted',
|
|
411
|
+
writePathGreenCites(writePath)
|
|
412
|
+
);
|
|
413
|
+
} else {
|
|
414
|
+
line(
|
|
415
|
+
warn,
|
|
416
|
+
repairEnvelope
|
|
392
417
|
? 'Repair: envelope may emit (`--hook-repair`); reinjection not guaranteed (advisory host)'
|
|
393
418
|
: 'Repair: no hard-boundary payload'
|
|
394
|
-
|
|
419
|
+
);
|
|
420
|
+
}
|
|
395
421
|
if (writePath.gap) {
|
|
396
422
|
line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message);
|
|
397
423
|
if (writePath.gap.fix) {
|
|
@@ -402,7 +428,12 @@ export function printDoctorDetailsHuman(view) {
|
|
|
402
428
|
console.log('');
|
|
403
429
|
console.log(color.bold('Gates & skills'));
|
|
404
430
|
if (listedMissing.length === 0 && !skippableCi) {
|
|
405
|
-
|
|
431
|
+
citedGreen(
|
|
432
|
+
line,
|
|
433
|
+
{ ok, warn },
|
|
434
|
+
'Shared gate artifacts found on disk; runtime activation is reported separately',
|
|
435
|
+
foundGateCites(view)
|
|
436
|
+
);
|
|
406
437
|
} else {
|
|
407
438
|
if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
|
|
408
439
|
if (skippableCi) line(warn, skippableCi);
|
|
@@ -417,7 +448,11 @@ export function printDoctorDetailsHuman(view) {
|
|
|
417
448
|
);
|
|
418
449
|
const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
|
|
419
450
|
const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
|
|
420
|
-
if (remMiss + remStale === 0 && !legacyCodex)
|
|
451
|
+
if (remMiss + remStale === 0 && !legacyCodex) {
|
|
452
|
+
citedGreen(line, { ok, warn }, '/ark-* skills current for detected tools', [
|
|
453
|
+
'.agents/skills',
|
|
454
|
+
]);
|
|
455
|
+
}
|
|
421
456
|
if (legacyCodex) {
|
|
422
457
|
line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)');
|
|
423
458
|
}
|
|
@@ -467,10 +502,17 @@ export function printDoctorDetailsHuman(view) {
|
|
|
467
502
|
console.log('');
|
|
468
503
|
console.log(color.bold('Baseline'));
|
|
469
504
|
if (!baseline.exists) {
|
|
470
|
-
|
|
505
|
+
const none = !analysisComplete
|
|
506
|
+
? 'No baseline — current violations were not fully evaluated'
|
|
507
|
+
: violations.length > 0
|
|
508
|
+
? 'No baseline — adopting a dirty repo? freeze with --update-baseline --force --contract-session --author <steward>'
|
|
509
|
+
: 'No baseline (nothing to freeze)';
|
|
510
|
+
if (!analysisComplete || violations.length > 0) line(warn, none);
|
|
511
|
+
else line(' ', none);
|
|
471
512
|
} else {
|
|
472
|
-
const
|
|
473
|
-
|
|
513
|
+
const baseClaim = `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`;
|
|
514
|
+
if (!analysisComplete || baselineHonesty.dirtyBaselineRisk) line(warn, baseClaim);
|
|
515
|
+
else citedGreen(line, { ok, warn }, baseClaim, ['.ark-baseline.json']);
|
|
474
516
|
if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
|
|
475
517
|
line(warn, baselineHonesty.message);
|
|
476
518
|
}
|
|
@@ -481,7 +523,9 @@ export function printDoctorDetailsHuman(view) {
|
|
|
481
523
|
|
|
482
524
|
console.log('');
|
|
483
525
|
console.log(color.bold('Command runners'));
|
|
484
|
-
if (staleRunners.length === 0)
|
|
526
|
+
if (staleRunners.length === 0) {
|
|
527
|
+
citedGreen(line, { ok, warn }, 'Emitted commands match the package manager', ['package.json']);
|
|
528
|
+
}
|
|
485
529
|
else {
|
|
486
530
|
line(warn, `Stale runner in ${staleRunners.join(', ')}`);
|
|
487
531
|
}
|
|
@@ -489,9 +533,11 @@ export function printDoctorDetailsHuman(view) {
|
|
|
489
533
|
console.log('');
|
|
490
534
|
console.log(color.bold('Adoption (separate from fitness score)'));
|
|
491
535
|
if (adoption.gaps.length === 0 && !adoption.layerBalance) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
536
|
+
citedGreen(
|
|
537
|
+
line,
|
|
538
|
+
{ ok, warn },
|
|
539
|
+
'Hosts, MCP argv, core optionality, origin report, baseline policy, and deploy-path lint/types look complete',
|
|
540
|
+
['AGENTS.md', 'ark.config.json']
|
|
495
541
|
);
|
|
496
542
|
} else {
|
|
497
543
|
for (const gap of adoption.gaps) {
|
|
@@ -540,7 +586,9 @@ export function printDoctorDetailsHuman(view) {
|
|
|
540
586
|
['Rules with peerIsolation: false', safety.disabledPeerIsolationRules],
|
|
541
587
|
];
|
|
542
588
|
for (const [label, entries] of rows) {
|
|
543
|
-
|
|
589
|
+
const claim = `${label}: ${entries.length}`;
|
|
590
|
+
if (entries.length === 0) line(' ', claim);
|
|
591
|
+
else line(warn, claim);
|
|
544
592
|
}
|
|
545
593
|
}
|
|
546
594
|
}
|
|
@@ -34,6 +34,9 @@ export function collectDoctorNextActions(ctx) {
|
|
|
34
34
|
) {
|
|
35
35
|
actions.push(nudge.nextAction);
|
|
36
36
|
}
|
|
37
|
+
if (ctx.layerOwners?.required && ctx.layerOwners.nextAction) {
|
|
38
|
+
actions.push(ctx.layerOwners.nextAction);
|
|
39
|
+
}
|
|
37
40
|
const enforceEmptyPlan =
|
|
38
41
|
ctx.operatingMode === 'enforce' && planAEmpty && gatesInstalled && !notAdopted;
|
|
39
42
|
if (enforceEmptyPlan) {
|
|
@@ -42,6 +45,26 @@ export function collectDoctorNextActions(ctx) {
|
|
|
42
45
|
'/ark-explore, then one small refactor with /ark-autopilot and your OK'
|
|
43
46
|
);
|
|
44
47
|
}
|
|
48
|
+
if (ctx.adrPresence?.missing && ctx.adrPresence.nextAction) {
|
|
49
|
+
actions.push(ctx.adrPresence.nextAction);
|
|
50
|
+
}
|
|
51
|
+
if (ctx.statusTransitionCatalog?.nextAction) {
|
|
52
|
+
actions.push(ctx.statusTransitionCatalog.nextAction);
|
|
53
|
+
} else if (ctx.statesTransitions?.nextAction) {
|
|
54
|
+
actions.push(ctx.statesTransitions.nextAction);
|
|
55
|
+
}
|
|
56
|
+
if (ctx.noDomainFrontend?.nextAction) {
|
|
57
|
+
actions.push(ctx.noDomainFrontend.nextAction);
|
|
58
|
+
}
|
|
59
|
+
if (ctx.prototypeShortcuts?.nextAction) {
|
|
60
|
+
actions.push(ctx.prototypeShortcuts.nextAction);
|
|
61
|
+
}
|
|
62
|
+
if (ctx.invariantTestsPath?.missing && ctx.invariantTestsPath.nextAction) {
|
|
63
|
+
actions.push(ctx.invariantTestsPath.nextAction);
|
|
64
|
+
}
|
|
65
|
+
if (ctx.invariantCoverageRoots?.missing && ctx.invariantCoverageRoots.nextAction) {
|
|
66
|
+
actions.push(ctx.invariantCoverageRoots.nextAction);
|
|
67
|
+
}
|
|
45
68
|
if (!ctx.analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
|
|
46
69
|
if (ctx.designSmells.length > 0 && ctx.postGreenPath) actions.push(ctx.postGreenPath.action);
|
|
47
70
|
if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -15,7 +15,13 @@ import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
|
15
15
|
import { detectAgentHomeGaps } from './agent-homes.mjs';
|
|
16
16
|
import { collectDoctorNextActions } from './doctor-next-actions.mjs';
|
|
17
17
|
import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
|
|
18
|
-
import { placementDescriptionFields } from './layer-description.mjs';
|
|
18
|
+
import { collectLayerOwnerResidual, layerGuidanceLine, placementDescriptionFields } from './layer-description.mjs';
|
|
19
|
+
import { collectAdrPresenceResidual, printAdrPresenceHint } from './adr-presence.mjs';
|
|
20
|
+
import { collectStatesTransitionsResidual } from './states-transitions-presence.mjs';
|
|
21
|
+
import { collectStatusTransitionCatalogResidual } from './status-transition-catalog.mjs';
|
|
22
|
+
import { collectNoDomainFrontendResidual } from './no-domain-frontend.mjs';
|
|
23
|
+
import { collectInvariantCoverageResiduals } from './invariant-tests-path.mjs';
|
|
24
|
+
export { printAdrPresenceHint };
|
|
19
25
|
export { printDoctorCompactHuman, printDoctorDetailsHuman };
|
|
20
26
|
export { summarizeRulesUnderContract };
|
|
21
27
|
|
|
@@ -68,6 +74,7 @@ import { writeCiMergeBoundary } from './ci-merge-boundary.mjs';
|
|
|
68
74
|
import {
|
|
69
75
|
classifyAdopted,
|
|
70
76
|
githubEvidenceForCiMergeBoundary,
|
|
77
|
+
isAdopted,
|
|
71
78
|
readAdoptionStance,
|
|
72
79
|
NOT_ADOPTED_NEXT_ACTION,
|
|
73
80
|
} from './adoption-stance.mjs';
|
|
@@ -173,7 +180,8 @@ export function runCoverage(root, config, files, rules, asJson) {
|
|
|
173
180
|
console.log(` ${pad('Layer')} Files`);
|
|
174
181
|
for (const row of layerRows) {
|
|
175
182
|
const flag = row.files === 0 ? ' (pattern matches nothing)' : '';
|
|
176
|
-
const
|
|
183
|
+
const guidance = layerGuidanceLine(row);
|
|
184
|
+
const caption = guidance ? ` ${guidance}` : '';
|
|
177
185
|
console.log(` ${pad(row.name)} ${String(row.files).padStart(5)}${flag}${caption}`);
|
|
178
186
|
}
|
|
179
187
|
console.log(` ${pad('(unclassified)')} ${String(unclassified.length).padStart(5)}`);
|
|
@@ -616,6 +624,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
616
624
|
options.facts ?? options.architectureFacts,
|
|
617
625
|
activeViolations
|
|
618
626
|
);
|
|
627
|
+
const layerOwners = collectLayerOwnerResidual(config);
|
|
619
628
|
const rulesUnderContract = doctorAdvisories.rulesUnderContract;
|
|
620
629
|
const arkRun = doctorAdvisories.arkRun;
|
|
621
630
|
// Single residual expression (nextPilot || extractionCard) — HTML report uses the same.
|
|
@@ -636,6 +645,14 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
636
645
|
github: githubForBoundary,
|
|
637
646
|
ci: ciMergeBoundary?.ci,
|
|
638
647
|
});
|
|
648
|
+
const adrPresence = collectAdrPresenceResidual({
|
|
649
|
+
root,
|
|
650
|
+
demanded: options.requireGates === true || adopted === 'required-merge',
|
|
651
|
+
});
|
|
652
|
+
const statesTransitions = collectStatesTransitionsResidual({ root });
|
|
653
|
+
const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
|
|
654
|
+
const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
|
|
655
|
+
const { invariantCoverageRoots, invariantTestsPath } = collectInvariantCoverageResiduals({ adopted: isAdopted(adopted) || options.requireGates === true, coverage: config?.coverage, config, root });
|
|
639
656
|
const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
|
|
640
657
|
computeDoctorEnforcementHonesty({
|
|
641
658
|
governedPercent: cov.governed.percent,
|
|
@@ -671,6 +688,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
671
688
|
selfHost:
|
|
672
689
|
packageVersionTruth?.selfHost === true ||
|
|
673
690
|
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
691
|
+
nativeFailClosed: writePath.nativeFailClosed,
|
|
692
|
+
nativeFailClosedPolicy: writePath.nativeFailClosedPolicy,
|
|
674
693
|
adopted,
|
|
675
694
|
ciMergeBoundary,
|
|
676
695
|
github: githubForBoundary,
|
|
@@ -763,6 +782,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
763
782
|
governed: cov.governed,
|
|
764
783
|
coverageHonesty,
|
|
765
784
|
layers: cov.layers,
|
|
785
|
+
...(layerOwners ? { layerOwners } : {}),
|
|
786
|
+
...(adrPresence ? { adrPresence } : {}),
|
|
787
|
+
...(statesTransitions ? { statesTransitions } : {}),
|
|
788
|
+
...(statusTransitionCatalog ? { statusTransitionCatalog } : {}), ...(noDomainFrontend ? { noDomainFrontend } : {}), ...(invariantTestsPath ? { invariantTestsPath } : {}), ...(invariantCoverageRoots ? { invariantCoverageRoots } : {}),
|
|
766
789
|
emptyLayers: cov.emptyLayers,
|
|
767
790
|
layersWithoutRules: cov.layersWithoutRules,
|
|
768
791
|
ungovernedDirs: cov.suggestions.length,
|
|
@@ -827,6 +850,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
827
850
|
mcpPresent: writePath.mcpPresent,
|
|
828
851
|
evidence: writePath.evidence,
|
|
829
852
|
honesty: writePathHonesty,
|
|
853
|
+
nativeFailClosed: writePath.nativeFailClosed ?? null,
|
|
854
|
+
nativeFailClosedPolicy: writePath.nativeFailClosedPolicy ?? 'unsupported',
|
|
830
855
|
...(writePath.sessionNote ? { sessionNote: writePath.sessionNote } : {}),
|
|
831
856
|
...(writePath.gap
|
|
832
857
|
? {
|
|
@@ -902,12 +927,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
902
927
|
operatingMode,
|
|
903
928
|
adopted,
|
|
904
929
|
stewardNudge: doctorAdvisories.stewardNudge,
|
|
930
|
+
layerOwners,
|
|
931
|
+
adrPresence,
|
|
932
|
+
statesTransitions,
|
|
933
|
+
statusTransitionCatalog, noDomainFrontend, prototypeShortcuts: doctorAdvisories.prototypeShortcuts, invariantTestsPath, invariantCoverageRoots,
|
|
905
934
|
});
|
|
906
935
|
const humanView = {
|
|
907
936
|
root,
|
|
908
937
|
analysisComplete,
|
|
909
938
|
completeness,
|
|
910
939
|
doctorAdvisories,
|
|
940
|
+
layerOwners,
|
|
941
|
+
adrPresence,
|
|
942
|
+
statesTransitions,
|
|
943
|
+
statusTransitionCatalog, noDomainFrontend, prototypeShortcuts: doctorAdvisories.prototypeShortcuts, invariantTestsPath, invariantCoverageRoots,
|
|
911
944
|
operatingMode,
|
|
912
945
|
designFitness,
|
|
913
946
|
adopted,
|