arkgate 4.8.15 → 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 +39 -0
- package/README.md +7 -5
- package/bin/ark-shared.mjs +2 -0
- package/bin/ark.mjs +17 -8
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/architecture-scan.mjs +17 -0
- package/bin/lib/baseline-key.mjs +2 -0
- package/bin/lib/config-contract.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +2 -0
- package/bin/lib/doctor-advisories.mjs +26 -0
- package/bin/lib/doctor-green-cite.mjs +139 -0
- package/bin/lib/doctor-human.mjs +98 -63
- package/bin/lib/doctor-next-actions.mjs +9 -0
- package/bin/lib/doctor-plan.mjs +6 -3
- package/bin/lib/field-install.mjs +47 -7
- package/bin/lib/first-run-help.mjs +1 -0
- package/bin/lib/improvement-compass-doctor.mjs +3 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/invariant-coverage.mjs +121 -0
- package/bin/lib/invariant-tests-path.mjs +212 -0
- package/bin/lib/package-manager.mjs +8 -0
- package/bin/lib/prototype-shortcuts.mjs +224 -0
- package/bin/lib/remediation.mjs +11 -0
- package/bin/lib/start-preview.mjs +20 -1
- package/dist/{configTypes-Dt3DpVbd.d.ts → configTypes-VD0qcubY.d.ts} +2 -1
- package/dist/{diagnosticCatalog-BEg85XlE.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 +31 -31
- package/dist/index.d.ts +5 -5
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +10 -10
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +10 -10
- package/dist/{types-TBiv0WHL.d.ts → types-BSzRy2X1.d.ts} +1 -1
- package/dist/{types-CN9tVMPz.d.ts → types-D5GT5ZT8.d.ts} +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +2 -0
- package/docs/configuration.md +11 -3
- package/docs/develop.md +3 -1
- package/docs/diagnostics.md +25 -1
- package/docs/package-surface.md +1 -1
- package/docs/use.md +5 -3
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +15 -0
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -1
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
- package/templates/agent-skills/ark-explain/SKILL.md +2 -1
- package/templates/agent-skills/ark-explore/SKILL.md +19 -0
- package/templates/agent-skills/ark-place/SKILL.md +4 -0
- package/templates/skills/ark-adopt.md +15 -0
- package/templates/skills/ark-autopilot.md +6 -1
- package/templates/skills/ark-coverage.md +1 -1
- package/templates/skills/ark-explain.md +2 -1
- package/templates/skills/ark-explore.md +19 -0
- package/templates/skills/ark-place.md +4 -0
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
invariantIdsFromCatalog,
|
|
17
17
|
loadInvariantCoverageInputs,
|
|
18
18
|
} from './invariant-coverage-io.mjs';
|
|
19
|
+
import { catalogHasEnforcedInvariant } from './invariant-coverage.mjs';
|
|
20
|
+
import {
|
|
21
|
+
declaredCoverageRootsPresent,
|
|
22
|
+
declaredInvariantTestsPathPresent,
|
|
23
|
+
scanDemandsInvariantTestsPath,
|
|
24
|
+
} from './invariant-tests-path.mjs';
|
|
19
25
|
import { loadArkRuleFileHints } from './arkrule-file-hints.mjs';
|
|
20
26
|
import { collectGovernedFiles } from './scan-files.mjs';
|
|
21
27
|
|
|
@@ -198,9 +204,20 @@ export function resolveArchitectureSnapshot({
|
|
|
198
204
|
filterHintPreload(coverageInputs?.fileContents, scoped),
|
|
199
205
|
scoped
|
|
200
206
|
);
|
|
207
|
+
const adopted = scanDemandsInvariantTestsPath(root, args);
|
|
208
|
+
const pathPresent = adopted
|
|
209
|
+
? declaredInvariantTestsPathPresent(root, effectiveConfig.coverage)
|
|
210
|
+
: true;
|
|
211
|
+
// Silent when nothing is enforced — do not probe coverageRoots on that path.
|
|
212
|
+
const rootsPresent = catalogHasEnforcedInvariant(arkRulesLoad.arkRules?.invariants)
|
|
213
|
+
? declaredCoverageRootsPresent(root, effectiveConfig.coverage)
|
|
214
|
+
: true;
|
|
201
215
|
const analyzed = analyzeTrustedResolvedProject({
|
|
202
216
|
contract: analysisContract,
|
|
203
217
|
facts,
|
|
218
|
+
...(adopted ? { adopted: true } : {}),
|
|
219
|
+
...(adopted && pathPresent === false ? { invariantTestsPathPresent: false } : {}),
|
|
220
|
+
...(rootsPresent === false ? { coverageRootsPresent: false } : {}),
|
|
204
221
|
...(coverageInputs ? { coverageInputs } : {}),
|
|
205
222
|
...(fileHints ? { fileHints } : {}),
|
|
206
223
|
});
|
package/bin/lib/baseline-key.mjs
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
export const NON_FREEZABLE_BASELINE_RULE_IDS = [
|
|
13
13
|
'ARKRULE_SCOPE_EMPTY',
|
|
14
14
|
'INVARIANT_CATALOG_EMPTY',
|
|
15
|
+
'INVARIANT_TESTS_PATH_MISSING',
|
|
16
|
+
'INVARIANT_COVERAGE_ROOTS_MISSING',
|
|
15
17
|
];
|
|
16
18
|
/**
|
|
17
19
|
* STRUCTURE freeze `target`: sensor id, plus `:symbol` when a method/class is known.
|
|
@@ -243,7 +243,7 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
243
243
|
coverage: {
|
|
244
244
|
type: 'object',
|
|
245
245
|
additionalProperties: false,
|
|
246
|
-
description: 'Invariant coverage scan controls. testGlobs replaces the built-in test-name heuristic; maxFiles raises or lowers the evidence file budget and also bounds structural-hint preload for orchestration-only, thin-adapter, and writes-via-aggregate (default 400; there is no arkrules.hintBudget); coverageRoots declares where the project runs its tests, so a covering test found outside them is reported instead of silently certifying an invariant.',
|
|
246
|
+
description: 'Invariant coverage scan controls. testGlobs replaces the built-in test-name heuristic; maxFiles raises or lowers the evidence file budget and also bounds structural-hint preload for orchestration-only, thin-adapter, and writes-via-aggregate (default 400; there is no arkrules.hintBudget); coverageRoots declares where the project runs its tests, so a covering test found outside them is reported instead of silently certifying an invariant. When any invariant is enforced, missing coverageRoots fails closed.',
|
|
247
247
|
properties: {
|
|
248
248
|
testGlobs: { ...stringArraySchema, minItems: 1 },
|
|
249
249
|
maxFiles: {
|
|
@@ -59,6 +59,8 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
59
59
|
entry('INVARIANT_CATALOG_EMPTY', 'arkrules', 'Domain invariant catalog is empty', 'ArkRules is on and a Domain-role layer has code, but invariants[] has no phrases the code must preserve. Empty looks like “done” until someone fills the catalog.', 'Add 1–2 short phrases to invariants[] in arkrules/<Domain>.json (or the mapped file). Starters show the shape. Advisory unless a domain structure rule is already enforced — then --strict-merge can refuse. Do not freeze this finding.', { oftenAdvisory: true }),
|
|
60
60
|
entry('INVARIANT_UNCOVERED', 'arkrules', 'Invariant without coverage evidence', 'An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial). Kind is never-had-tests (adopt residual) vs tests-disappeared (suite exists).', 'Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green. When the message reports an exhausted file budget, raise coverage.maxFiles (or narrow coverage.testGlobs) in ark.config.json.'),
|
|
61
61
|
entry('INVARIANT_COVERAGE_OUTSIDE_ROOTS', 'arkrules', 'Covering test outside the declared coverage roots', 'The only test naming this invariant sits outside coverage.coverageRoots — the places the project declares its runner executes. ArkGate matches declared text and never executes tests, so it cannot tell whether that file is ever run: coverage there is a test that exists, not a test that runs.', 'Move the test under a declared coverage root, or add its root to coverage.coverageRoots in ark.config.json. Advisory: it never fails strict, but promotion to enforced refuses on it.', { oftenAdvisory: true }),
|
|
62
|
+
entry('INVARIANT_TESTS_PATH_MISSING', 'arkrules', 'Domain-invariant tests path missing under adopted', 'The project is adopted (required CI or explicit advisory) and has domain invariants that want test evidence, but ark.config.json does not name where those tests live. Green coverage without a real tests path is an empty checkbox.', 'Add coverage.testGlobs or coverage.coverageRoots in ark.config.json pointing at a real tests folder, then re-run. Adopted mode fails closed until that path is present. Not freezable.'),
|
|
63
|
+
entry('INVARIANT_COVERAGE_ROOTS_MISSING', 'arkrules', 'Coverage roots missing while an invariant is enforced', 'A catalogued domain invariant is enforced, but ark.config.json does not name coverage.coverageRoots — the folders where the project says its test runner actually goes. Without that declaration, INVARIANT_COVERAGE_OUTSIDE_ROOTS cannot fire and coverage can certify a test no runner runs.', 'Add coverage.coverageRoots in ark.config.json pointing at the folder the test runner uses, then re-run. testGlobs alone is not enough. Fail-closed until that path is present. Not freezable.'),
|
|
62
64
|
// ── ArkRun (opt-in extra; RN05 dual-depth nextAction) ────────────────────
|
|
63
65
|
entry('ARKRUN_MISSING_ROOT', 'arkrun', 'No kernel factory in composition roots', 'The ArkRun extra is on but no createArkKernel / createStrictArkKernel / createArkKernelFromConfig / createStrictArkKernelFromConfig factory was found in arkRun.compositionRoots, so agents can skip the kernel while the write gate stays green.', 'Import createStrictArkKernel from arkgate/runtime (same npm package; @arkgate/runtime is deprecated) and call it in a composition root listed in arkRun.compositionRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.'),
|
|
64
66
|
entry('ARKRUN_KERNEL_IN_DOMAIN', 'arkrun', 'Domain-role layer imports the kernel', 'A Domain-role layer imports arkgate/runtime, @arkgate/runtime, or kernel types. Domain stays kernel-free; composition roots and adapters own the factory.', 'Move the kernel import out of the Domain-role layer into a composition root or adapter. Import from arkgate/runtime (same npm package; @arkgate/runtime is deprecated), then preflight again. Never mechanical-safe.'),
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
summarizeArkOrderSection,
|
|
37
37
|
} from './ark-order-doctor.mjs';
|
|
38
38
|
import { composeMergePlanesHonesty } from './extra-merge-teeth.mjs';
|
|
39
|
+
import { collectPrototypeShortcutsResidual } from './prototype-shortcuts.mjs';
|
|
39
40
|
|
|
40
41
|
export function attachExtraDoctorSections(rulesUnderContract, config, classification, findings) {
|
|
41
42
|
const arkRulesMerge = {
|
|
@@ -127,6 +128,24 @@ export function printCompactExtraDoctorLines(advisories, io) {
|
|
|
127
128
|
io.line(io.warn, noDomain.ask);
|
|
128
129
|
if (noDomain.nextAction) io.line(' ', `Next: ${noDomain.nextAction}`);
|
|
129
130
|
}
|
|
131
|
+
const prototypeShortcuts = advisories?.prototypeShortcuts;
|
|
132
|
+
if (prototypeShortcuts?.ask) {
|
|
133
|
+
console.log('');
|
|
134
|
+
io.line(io.warn, prototypeShortcuts.ask);
|
|
135
|
+
if (prototypeShortcuts.nextAction) io.line(' ', `Next: ${prototypeShortcuts.nextAction}`);
|
|
136
|
+
}
|
|
137
|
+
const testsPath = advisories?.invariantTestsPath;
|
|
138
|
+
if (testsPath?.missing && testsPath.ask) {
|
|
139
|
+
console.log('');
|
|
140
|
+
io.line(io.warn, testsPath.ask);
|
|
141
|
+
if (testsPath.nextAction) io.line(' ', `Next: ${testsPath.nextAction}`);
|
|
142
|
+
}
|
|
143
|
+
const coverageRoots = advisories?.invariantCoverageRoots;
|
|
144
|
+
if (coverageRoots?.missing && coverageRoots.ask) {
|
|
145
|
+
console.log('');
|
|
146
|
+
io.line(io.warn, coverageRoots.ask);
|
|
147
|
+
if (coverageRoots.nextAction) io.line(' ', `Next: ${coverageRoots.nextAction}`);
|
|
148
|
+
}
|
|
130
149
|
const rulesUnderContract = advisories?.rulesUnderContract;
|
|
131
150
|
const arkRulesLines = formatArkRulesDoctorLines(rulesUnderContract);
|
|
132
151
|
if (arkRulesLines.length > 0) {
|
|
@@ -221,7 +240,14 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
|
|
|
221
240
|
classification,
|
|
222
241
|
activeViolations
|
|
223
242
|
);
|
|
243
|
+
const prototypeShortcuts = collectPrototypeShortcutsResidual({
|
|
244
|
+
root,
|
|
245
|
+
config,
|
|
246
|
+
coverage: cov,
|
|
247
|
+
files,
|
|
248
|
+
});
|
|
224
249
|
return {
|
|
250
|
+
...(prototypeShortcuts ? { prototypeShortcuts } : {}),
|
|
225
251
|
contractHealth: computeContractHealth(root, config, cov, rules),
|
|
226
252
|
ambientState: computeAmbientState(ts, root, config, files),
|
|
227
253
|
physicalCohesion,
|
|
@@ -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,24 +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 { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
|
|
17
16
|
import { layerGuidanceLine } from './layer-description.mjs';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function ciNotFailClosedNotice(view) {
|
|
30
|
-
const file = view?.ciNotFailClosed?.workflowFile;
|
|
31
|
-
if (!file) return null;
|
|
32
|
-
return `CI not fail-closed: ${file} — remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only`;
|
|
33
|
-
}
|
|
17
|
+
import {
|
|
18
|
+
citedGreen,
|
|
19
|
+
ciMergeGreenCites,
|
|
20
|
+
ciNotFailClosedNotice,
|
|
21
|
+
displayedMissingGates,
|
|
22
|
+
foundGateCites,
|
|
23
|
+
printHealthyHeadline,
|
|
24
|
+
writePathGreenCites,
|
|
25
|
+
} from './doctor-green-cite.mjs';
|
|
34
26
|
|
|
35
27
|
function lineWith(ok, warn, bad, color) {
|
|
36
28
|
return (mark, text) => console.log(` ${mark} ${text}`);
|
|
@@ -97,14 +89,13 @@ export function printDoctorCompactHuman(view) {
|
|
|
97
89
|
: 'import rules check out. Keep host + CI.',
|
|
98
90
|
};
|
|
99
91
|
const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
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);
|
|
108
99
|
if (emptyScope) {
|
|
109
100
|
line(
|
|
110
101
|
bad,
|
|
@@ -114,17 +105,18 @@ export function printDoctorCompactHuman(view) {
|
|
|
114
105
|
|
|
115
106
|
console.log('');
|
|
116
107
|
if (ciMergeBoundary?.ci?.state) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}
|
|
121
114
|
}
|
|
122
115
|
if (adopted === 'advisory-only-acked') {
|
|
123
116
|
line(warn, 'Adoption: advisory-only ack — not a required GitHub status.');
|
|
124
117
|
}
|
|
125
118
|
if (isDoctorHealthyNothingToDo(designFitness, uniqueActions, adopted)) {
|
|
126
|
-
|
|
127
|
-
console.log(color.dim(' Keep write path + CI.'));
|
|
119
|
+
printHealthyHeadline(view, color);
|
|
128
120
|
} else {
|
|
129
121
|
console.log(color.bold('Primary next action'));
|
|
130
122
|
console.log(` 1. ${uniqueActions[0]}`);
|
|
@@ -132,13 +124,10 @@ export function printDoctorCompactHuman(view) {
|
|
|
132
124
|
|
|
133
125
|
console.log('');
|
|
134
126
|
console.log(color.bold('Coverage'));
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
? ok
|
|
140
|
-
: warn;
|
|
141
|
-
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);
|
|
142
131
|
for (const row of cov.layers ?? []) {
|
|
143
132
|
const guidance = layerGuidanceLine(row);
|
|
144
133
|
if (guidance) line(' ', `${row.name} — ${guidance}`);
|
|
@@ -175,7 +164,7 @@ export function printDoctorCompactHuman(view) {
|
|
|
175
164
|
...doctorAdvisories,
|
|
176
165
|
layerOwners: view.layerOwners,
|
|
177
166
|
adrPresence: view.adrPresence,
|
|
178
|
-
statesTransitions: view.statesTransitions, statusTransitionCatalog: view.statusTransitionCatalog, noDomainFrontend: view.noDomainFrontend,
|
|
167
|
+
statesTransitions: view.statesTransitions, statusTransitionCatalog: view.statusTransitionCatalog, noDomainFrontend: view.noDomainFrontend, invariantTestsPath: view.invariantTestsPath, invariantCoverageRoots: view.invariantCoverageRoots,
|
|
179
168
|
},
|
|
180
169
|
{ line, warn }
|
|
181
170
|
);
|
|
@@ -261,7 +250,12 @@ export function printDoctorDetailsHuman(view) {
|
|
|
261
250
|
`Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
|
|
262
251
|
);
|
|
263
252
|
}
|
|
264
|
-
if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0)
|
|
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
|
+
}
|
|
265
259
|
const captioned = (cov.layers ?? []).filter((row) => layerGuidanceLine(row));
|
|
266
260
|
if (captioned.length > 0) {
|
|
267
261
|
console.log('');
|
|
@@ -288,7 +282,8 @@ export function printDoctorDetailsHuman(view) {
|
|
|
288
282
|
console.log('');
|
|
289
283
|
console.log(color.bold('Design fitness'));
|
|
290
284
|
if (designSmells.length === 0) {
|
|
291
|
-
|
|
285
|
+
if (analysisComplete) citedGreen(line, { ok, warn }, designFitness.label, ['ark.config.json']);
|
|
286
|
+
else line(warn, designFitness.label);
|
|
292
287
|
} else {
|
|
293
288
|
line(designFitness.designWeak ? warn : warn, designFitness.label);
|
|
294
289
|
for (const smell of designSmells.slice(0, 5)) {
|
|
@@ -355,15 +350,16 @@ export function printDoctorDetailsHuman(view) {
|
|
|
355
350
|
} else if (designFitness.designWeak) {
|
|
356
351
|
line(warn, `None on checked imports — import rules match the config; leftover design work remains (${modeTitle}). Not healthy finished.`);
|
|
357
352
|
} else {
|
|
358
|
-
line
|
|
353
|
+
citedGreen(line, { ok, warn }, 'None — the code matches the contract on checked edges', [
|
|
354
|
+
'ark.config.json',
|
|
355
|
+
]);
|
|
359
356
|
}
|
|
360
357
|
} else {
|
|
361
358
|
const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
|
|
362
359
|
const supNote = suppressed > 0 ? `, ${suppressed} frozen` : '';
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
);
|
|
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']);
|
|
367
363
|
for (const edge of summary.edges.slice(0, 3)) line(' ', color.dim(`${edge.count} ${edge.edge}`));
|
|
368
364
|
if (summary.concentrated && typeof summary.dominant === 'string' && summary.dominant.includes(' → ')) {
|
|
369
365
|
line(warn, color.dim(`${Math.round(summary.dominantShare * 100)}% on one edge (${summary.dominant}) — likely a contract fix, not debt`));
|
|
@@ -387,7 +383,17 @@ export function printDoctorDetailsHuman(view) {
|
|
|
387
383
|
: bad;
|
|
388
384
|
line(' ', `Active host: ${writePath.activeHost}`);
|
|
389
385
|
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
390
|
-
|
|
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
|
+
}
|
|
391
397
|
if (writePathHonesty.message) line(warn, writePathHonesty.message);
|
|
392
398
|
if (writePath.sessionNote) {
|
|
393
399
|
line(warn, writePath.sessionNote);
|
|
@@ -397,14 +403,21 @@ export function printDoctorDetailsHuman(view) {
|
|
|
397
403
|
const supportCaps = writePath.support?.capabilities || {};
|
|
398
404
|
const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
|
|
399
405
|
const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
:
|
|
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
|
|
405
417
|
? 'Repair: envelope may emit (`--hook-repair`); reinjection not guaranteed (advisory host)'
|
|
406
418
|
: 'Repair: no hard-boundary payload'
|
|
407
|
-
|
|
419
|
+
);
|
|
420
|
+
}
|
|
408
421
|
if (writePath.gap) {
|
|
409
422
|
line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message);
|
|
410
423
|
if (writePath.gap.fix) {
|
|
@@ -415,7 +428,12 @@ export function printDoctorDetailsHuman(view) {
|
|
|
415
428
|
console.log('');
|
|
416
429
|
console.log(color.bold('Gates & skills'));
|
|
417
430
|
if (listedMissing.length === 0 && !skippableCi) {
|
|
418
|
-
|
|
431
|
+
citedGreen(
|
|
432
|
+
line,
|
|
433
|
+
{ ok, warn },
|
|
434
|
+
'Shared gate artifacts found on disk; runtime activation is reported separately',
|
|
435
|
+
foundGateCites(view)
|
|
436
|
+
);
|
|
419
437
|
} else {
|
|
420
438
|
if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
|
|
421
439
|
if (skippableCi) line(warn, skippableCi);
|
|
@@ -430,7 +448,11 @@ export function printDoctorDetailsHuman(view) {
|
|
|
430
448
|
);
|
|
431
449
|
const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
|
|
432
450
|
const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
|
|
433
|
-
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
|
+
}
|
|
434
456
|
if (legacyCodex) {
|
|
435
457
|
line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)');
|
|
436
458
|
}
|
|
@@ -480,10 +502,17 @@ export function printDoctorDetailsHuman(view) {
|
|
|
480
502
|
console.log('');
|
|
481
503
|
console.log(color.bold('Baseline'));
|
|
482
504
|
if (!baseline.exists) {
|
|
483
|
-
|
|
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);
|
|
484
512
|
} else {
|
|
485
|
-
const
|
|
486
|
-
|
|
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']);
|
|
487
516
|
if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
|
|
488
517
|
line(warn, baselineHonesty.message);
|
|
489
518
|
}
|
|
@@ -494,7 +523,9 @@ export function printDoctorDetailsHuman(view) {
|
|
|
494
523
|
|
|
495
524
|
console.log('');
|
|
496
525
|
console.log(color.bold('Command runners'));
|
|
497
|
-
if (staleRunners.length === 0)
|
|
526
|
+
if (staleRunners.length === 0) {
|
|
527
|
+
citedGreen(line, { ok, warn }, 'Emitted commands match the package manager', ['package.json']);
|
|
528
|
+
}
|
|
498
529
|
else {
|
|
499
530
|
line(warn, `Stale runner in ${staleRunners.join(', ')}`);
|
|
500
531
|
}
|
|
@@ -502,9 +533,11 @@ export function printDoctorDetailsHuman(view) {
|
|
|
502
533
|
console.log('');
|
|
503
534
|
console.log(color.bold('Adoption (separate from fitness score)'));
|
|
504
535
|
if (adoption.gaps.length === 0 && !adoption.layerBalance) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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']
|
|
508
541
|
);
|
|
509
542
|
} else {
|
|
510
543
|
for (const gap of adoption.gaps) {
|
|
@@ -553,7 +586,9 @@ export function printDoctorDetailsHuman(view) {
|
|
|
553
586
|
['Rules with peerIsolation: false', safety.disabledPeerIsolationRules],
|
|
554
587
|
];
|
|
555
588
|
for (const [label, entries] of rows) {
|
|
556
|
-
|
|
589
|
+
const claim = `${label}: ${entries.length}`;
|
|
590
|
+
if (entries.length === 0) line(' ', claim);
|
|
591
|
+
else line(warn, claim);
|
|
557
592
|
}
|
|
558
593
|
}
|
|
559
594
|
}
|
|
@@ -56,6 +56,15 @@ export function collectDoctorNextActions(ctx) {
|
|
|
56
56
|
if (ctx.noDomainFrontend?.nextAction) {
|
|
57
57
|
actions.push(ctx.noDomainFrontend.nextAction);
|
|
58
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
|
+
}
|
|
59
68
|
if (!ctx.analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
|
|
60
69
|
if (ctx.designSmells.length > 0 && ctx.postGreenPath) actions.push(ctx.postGreenPath.action);
|
|
61
70
|
if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { collectAdrPresenceResidual, printAdrPresenceHint } from './adr-presence
|
|
|
20
20
|
import { collectStatesTransitionsResidual } from './states-transitions-presence.mjs';
|
|
21
21
|
import { collectStatusTransitionCatalogResidual } from './status-transition-catalog.mjs';
|
|
22
22
|
import { collectNoDomainFrontendResidual } from './no-domain-frontend.mjs';
|
|
23
|
+
import { collectInvariantCoverageResiduals } from './invariant-tests-path.mjs';
|
|
23
24
|
export { printAdrPresenceHint };
|
|
24
25
|
export { printDoctorCompactHuman, printDoctorDetailsHuman };
|
|
25
26
|
export { summarizeRulesUnderContract };
|
|
@@ -73,6 +74,7 @@ import { writeCiMergeBoundary } from './ci-merge-boundary.mjs';
|
|
|
73
74
|
import {
|
|
74
75
|
classifyAdopted,
|
|
75
76
|
githubEvidenceForCiMergeBoundary,
|
|
77
|
+
isAdopted,
|
|
76
78
|
readAdoptionStance,
|
|
77
79
|
NOT_ADOPTED_NEXT_ACTION,
|
|
78
80
|
} from './adoption-stance.mjs';
|
|
@@ -650,6 +652,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
650
652
|
const statesTransitions = collectStatesTransitionsResidual({ root });
|
|
651
653
|
const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
|
|
652
654
|
const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
|
|
655
|
+
const { invariantCoverageRoots, invariantTestsPath } = collectInvariantCoverageResiduals({ adopted: isAdopted(adopted) || options.requireGates === true, coverage: config?.coverage, config, root });
|
|
653
656
|
const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
|
|
654
657
|
computeDoctorEnforcementHonesty({
|
|
655
658
|
governedPercent: cov.governed.percent,
|
|
@@ -782,7 +785,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
782
785
|
...(layerOwners ? { layerOwners } : {}),
|
|
783
786
|
...(adrPresence ? { adrPresence } : {}),
|
|
784
787
|
...(statesTransitions ? { statesTransitions } : {}),
|
|
785
|
-
...(statusTransitionCatalog ? { statusTransitionCatalog } : {}), ...(noDomainFrontend ? { noDomainFrontend } : {}),
|
|
788
|
+
...(statusTransitionCatalog ? { statusTransitionCatalog } : {}), ...(noDomainFrontend ? { noDomainFrontend } : {}), ...(invariantTestsPath ? { invariantTestsPath } : {}), ...(invariantCoverageRoots ? { invariantCoverageRoots } : {}),
|
|
786
789
|
emptyLayers: cov.emptyLayers,
|
|
787
790
|
layersWithoutRules: cov.layersWithoutRules,
|
|
788
791
|
ungovernedDirs: cov.suggestions.length,
|
|
@@ -927,7 +930,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
927
930
|
layerOwners,
|
|
928
931
|
adrPresence,
|
|
929
932
|
statesTransitions,
|
|
930
|
-
statusTransitionCatalog, noDomainFrontend,
|
|
933
|
+
statusTransitionCatalog, noDomainFrontend, prototypeShortcuts: doctorAdvisories.prototypeShortcuts, invariantTestsPath, invariantCoverageRoots,
|
|
931
934
|
});
|
|
932
935
|
const humanView = {
|
|
933
936
|
root,
|
|
@@ -937,7 +940,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
937
940
|
layerOwners,
|
|
938
941
|
adrPresence,
|
|
939
942
|
statesTransitions,
|
|
940
|
-
statusTransitionCatalog, noDomainFrontend,
|
|
943
|
+
statusTransitionCatalog, noDomainFrontend, prototypeShortcuts: doctorAdvisories.prototypeShortcuts, invariantTestsPath, invariantCoverageRoots,
|
|
941
944
|
operatingMode,
|
|
942
945
|
designFitness,
|
|
943
946
|
adopted,
|