arkgate 4.8.3 → 4.8.4
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 +242 -0
- package/README.md +10 -3
- package/bin/ark-check-runtime.mjs +340 -5
- package/bin/ark-layer-match.mjs +170 -13
- package/bin/ark-mcp-runtime.mjs +9 -2
- package/bin/lib/analysis-completeness.mjs +86 -0
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/architecture-scan.mjs +2 -0
- package/bin/lib/arkrules-contract.mjs +8 -1
- package/bin/lib/check-args.mjs +66 -0
- package/bin/lib/config-contract.mjs +26 -0
- package/bin/lib/design-smells.mjs +85 -0
- package/bin/lib/diagnostic-catalog.mjs +6 -1
- package/bin/lib/first-run-help.mjs +12 -0
- package/bin/lib/invariant-coverage-io.mjs +175 -19
- package/bin/lib/invariant-coverage.mjs +110 -7
- package/bin/lib/literal-path-drift-io.mjs +569 -0
- package/bin/lib/literal-path-drift.mjs +761 -0
- package/bin/lib/policy-delta-io.mjs +5 -0
- package/bin/lib/remediation.mjs +15 -0
- package/bin/lib/rules-under-contract.mjs +5 -0
- package/bin/lib/scan-files.mjs +54 -0
- package/bin/lib/sensor-promote-cli.mjs +372 -0
- package/bin/lib/sensor-promote-io.mjs +246 -0
- package/bin/lib/sensor-promotion.mjs +363 -0
- package/dist/{configTypes-dNJ2C0yx.d.ts → configTypes-dy5PfTqS.d.ts} +31 -0
- package/dist/{diagnosticCatalog-C5GgeyEE.d.ts → diagnosticCatalog-DgTs0abp.d.ts} +75 -7
- package/dist/eslint/index.cjs +6 -6
- package/dist/eslint/index.d.ts +34 -1
- package/dist/eslint/index.js +6 -6
- package/dist/index.cjs +32 -32
- package/dist/index.d.ts +65 -4
- package/dist/index.js +29 -29
- 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-dK24fDZa.d.ts → types-BuM8WNqe.d.ts} +1 -1
- package/dist/{types-DeK7SYGC.d.ts → types-D95drJ3_.d.ts} +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +182 -0
- package/docs/configuration.md +77 -1
- package/docs/develop.md +1 -0
- package/docs/diagnostics.md +70 -1
- package/docs/package-surface.md +32 -2
- package/package.json +2 -2
- package/schemas/ark.config.schema.json +63 -0
- package/server.json +3 -3
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -0
- package/templates/skills/ark-adopt.md +5 -0
- package/templates/skills/ark-coverage.md +1 -0
|
@@ -8,6 +8,48 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/invariant-coverage.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Human-readable discard tail. Empty when the scan discarded nothing.
|
|
13
|
+
* `omitBudget` drops the budget clause and the load totals for messages whose
|
|
14
|
+
* own text already carries them — the same number twice reads as two facts.
|
|
15
|
+
*/
|
|
16
|
+
function formatCoverageDiscards(stats, omitBudget = false) {
|
|
17
|
+
if (!stats)
|
|
18
|
+
return '';
|
|
19
|
+
const d = stats.discarded;
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (d.budget > 0 && !omitBudget)
|
|
22
|
+
parts.push(`${d.budget} past the ${stats.maxFiles}-file budget`);
|
|
23
|
+
if (d.noInvariantMention > 0)
|
|
24
|
+
parts.push(`${d.noInvariantMention} naming no catalogued invariant`);
|
|
25
|
+
if (d.oversize > 0)
|
|
26
|
+
parts.push(`${d.oversize} over the per-file byte cap`);
|
|
27
|
+
if (d.unreadable > 0)
|
|
28
|
+
parts.push(`${d.unreadable} unreadable (files or directories)`);
|
|
29
|
+
if (d.depthLimited > 0)
|
|
30
|
+
parts.push(`${d.depthLimited} directories past the walk depth limit`);
|
|
31
|
+
if (d.outOfRoot > 0)
|
|
32
|
+
parts.push(`${d.outOfRoot} symlinked outside the project root`);
|
|
33
|
+
if (parts.length === 0)
|
|
34
|
+
return '';
|
|
35
|
+
const totals = omitBudget
|
|
36
|
+
? ''
|
|
37
|
+
: ` (loaded ${stats.filesLoaded} files, kept ${stats.testFilesRetained} tests)`;
|
|
38
|
+
return ` Scan discarded ${parts.join(', ')}${totals}.`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* True when `file` sits inside one of the declared coverage roots.
|
|
42
|
+
* A root is a path prefix, `.` (or `''`) meaning the whole project.
|
|
43
|
+
*/
|
|
44
|
+
function isUnderCoverageRoot(file, roots) {
|
|
45
|
+
const target = file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
46
|
+
return roots.some((rawRoot) => {
|
|
47
|
+
const root = rawRoot.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
48
|
+
if (root === '' || root === '.')
|
|
49
|
+
return true;
|
|
50
|
+
return target === root || target.startsWith(`${root}/`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
11
53
|
function titleMatchesInvariant(content, id) {
|
|
12
54
|
// Match describe/it/test string titles containing the invariant id.
|
|
13
55
|
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -39,20 +81,49 @@ export function evaluateInvariantCoverage(input) {
|
|
|
39
81
|
const testFiles = input.testFiles ?? [];
|
|
40
82
|
const testGlobsMissing = input.testGlobsMissing === true || testFiles.length === 0;
|
|
41
83
|
const coverageBudgetExhausted = input.coverageBudgetExhausted === true;
|
|
84
|
+
const stats = input.coverageStats;
|
|
85
|
+
const discardTail = formatCoverageDiscards(stats);
|
|
86
|
+
// The budget-exhausted sentence already carries the cap, the load and the
|
|
87
|
+
// discards at the cap, so its tail reports only the other discard reasons.
|
|
88
|
+
const budgetExhaustedTail = formatCoverageDiscards(stats, true);
|
|
89
|
+
// Numbers, not adjectives: a budget-exhausted verdict must say how big the
|
|
90
|
+
// budget was, what it bought, and which knob raises it.
|
|
91
|
+
const budgetDetail = stats
|
|
92
|
+
? `coverage file budget exhausted: ${stats.filesLoaded} files loaded at the ${stats.maxFiles}-file cap, ${stats.testFilesRetained} tests retained, ${stats.discarded.budget} files discarded at the cap; raise "coverage.maxFiles" in ark.config.json (the cap bounds files RETAINED as evidence${typeof stats.filesRead === 'number' ? `; ${stats.filesRead} were read` : ''})`
|
|
93
|
+
: 'coverage file budget exhausted';
|
|
94
|
+
const coverageRoots = (input.coverageRoots ?? []).filter((root) => typeof root === 'string' && root.length > 0);
|
|
95
|
+
const rootsDeclared = coverageRoots.length > 0;
|
|
96
|
+
const declaredRootsList = coverageRoots.join(', ');
|
|
42
97
|
const coverage = [];
|
|
43
98
|
const violations = [];
|
|
44
99
|
for (const inv of invariants) {
|
|
45
100
|
const evidence = [];
|
|
46
101
|
const wantsTest = inv.coverage?.test !== false; // default: prefer test evidence when catalogued
|
|
47
102
|
const symbol = inv.coverage?.symbol;
|
|
103
|
+
let testEvidenceFile;
|
|
104
|
+
let outsideDeclaredRoots;
|
|
48
105
|
if (!testGlobsMissing && wantsTest) {
|
|
106
|
+
// A covering test INSIDE a declared root wins over one outside it: the
|
|
107
|
+
// finding is "the only proof lives where the runner does not go", not
|
|
108
|
+
// "some proof lives there".
|
|
109
|
+
let fallbackOutside;
|
|
49
110
|
for (const file of testFiles) {
|
|
50
111
|
const content = input.fileContents[file];
|
|
51
|
-
if (content
|
|
52
|
-
|
|
112
|
+
if (!content || !titleMatchesInvariant(content, inv.id))
|
|
113
|
+
continue;
|
|
114
|
+
if (!rootsDeclared || isUnderCoverageRoot(file, coverageRoots)) {
|
|
115
|
+
testEvidenceFile = file;
|
|
116
|
+
outsideDeclaredRoots = rootsDeclared ? false : undefined;
|
|
53
117
|
break;
|
|
54
118
|
}
|
|
119
|
+
fallbackOutside ??= file;
|
|
120
|
+
}
|
|
121
|
+
if (testEvidenceFile === undefined && fallbackOutside !== undefined) {
|
|
122
|
+
testEvidenceFile = fallbackOutside;
|
|
123
|
+
outsideDeclaredRoots = true;
|
|
55
124
|
}
|
|
125
|
+
if (testEvidenceFile !== undefined)
|
|
126
|
+
evidence.push('test-title');
|
|
56
127
|
}
|
|
57
128
|
if (symbol && symbolPresent(input.fileContents, symbol)) {
|
|
58
129
|
evidence.push('symbol');
|
|
@@ -76,20 +147,44 @@ export function evaluateInvariantCoverage(input) {
|
|
|
76
147
|
evidence,
|
|
77
148
|
partial,
|
|
78
149
|
description: inv.description,
|
|
150
|
+
...(testEvidenceFile !== undefined ? { testEvidenceFile } : {}),
|
|
151
|
+
...(outsideDeclaredRoots !== undefined ? { outsideDeclaredRoots } : {}),
|
|
79
152
|
});
|
|
153
|
+
// The covering test exists but sits outside the roots the project declared
|
|
154
|
+
// its runner walks. ArkGate does not execute tests, so it cannot tell the
|
|
155
|
+
// difference — it can only report that the two declarations disagree.
|
|
156
|
+
if (outsideDeclaredRoots === true && testEvidenceFile !== undefined) {
|
|
157
|
+
violations.push({
|
|
158
|
+
ruleId: 'INVARIANT_COVERAGE_OUTSIDE_ROOTS',
|
|
159
|
+
message: `Invariant ${inv.id} is covered only by ${testEvidenceFile}, which is outside the declared coverage roots (${declaredRootsList}). ` +
|
|
160
|
+
'ArkGate matches declared text and never executes tests, so it cannot tell whether that file is run: move the test under a declared root, or add its root to "coverage.coverageRoots" in ark.config.json.',
|
|
161
|
+
file: testEvidenceFile,
|
|
162
|
+
line: 1,
|
|
163
|
+
arkruleId: inv.id,
|
|
164
|
+
arkruleSource: inv.provenance.sourceFile,
|
|
165
|
+
fromLayer: inv.provenance.layer,
|
|
166
|
+
severity: 'warning',
|
|
167
|
+
failsStrict: false,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
80
170
|
if (!covered || partial) {
|
|
81
171
|
// Enforced + proven uncovered → failsStrict; partial always advisory (never fake green).
|
|
82
172
|
const failsStrict = inv.mode === 'enforced' && !partial;
|
|
83
173
|
const kind = testGlobsMissing || testFiles.length === 0 ? 'never-had-tests' : 'tests-disappeared';
|
|
84
174
|
violations.push({
|
|
85
175
|
ruleId: 'INVARIANT_UNCOVERED',
|
|
86
|
-
message: partial
|
|
176
|
+
message: (partial
|
|
87
177
|
? coverageBudgetExhausted
|
|
88
|
-
? `Invariant ${inv.id} coverage cannot be proven (
|
|
178
|
+
? `Invariant ${inv.id} coverage cannot be proven (${budgetDetail}); reporting partial, not covered.`
|
|
89
179
|
: `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered (never-had-tests).`
|
|
90
|
-
:
|
|
91
|
-
|
|
92
|
-
|
|
180
|
+
: // Say what was actually checked. "Not covered by a test
|
|
181
|
+
// title" reads as "there is no test", and its inverse
|
|
182
|
+
// reads as "there is a test and it runs" — neither is
|
|
183
|
+
// something a text match can know.
|
|
184
|
+
kind === 'tests-disappeared'
|
|
185
|
+
? `Invariant ${inv.id}: no scanned test names it in a describe/it title and no declared symbol was found (tests-disappeared — a suite exists). ArkGate matches declared text; it never executes tests.`
|
|
186
|
+
: `Invariant ${inv.id}: no scanned test names it in a describe/it title and no declared symbol was found (never-had-tests — the scan found no tests at all). ArkGate matches declared text; it never executes tests.`) +
|
|
187
|
+
(partial && coverageBudgetExhausted ? budgetExhaustedTail : discardTail),
|
|
93
188
|
file: inv.provenance.sourceFile,
|
|
94
189
|
line: 1,
|
|
95
190
|
arkruleId: inv.id,
|
|
@@ -130,5 +225,13 @@ export function canPromoteInvariant(coverage) {
|
|
|
130
225
|
reason: `Invariant ${coverage.invariantId} is uncovered; add a test title or symbol before promoting to enforced.`,
|
|
131
226
|
};
|
|
132
227
|
}
|
|
228
|
+
// Promotion is the moment coverage stops being advice, so an evidence file
|
|
229
|
+
// the project itself says its runner does not walk cannot carry it.
|
|
230
|
+
if (coverage.outsideDeclaredRoots === true) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
reason: `Invariant ${coverage.invariantId} is covered only by ${coverage.testEvidenceFile ?? 'a test'}, outside the declared coverage roots; ArkGate cannot tell whether that test runs, so it will not promote on it.`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
133
236
|
return { ok: true, reason: `Invariant ${coverage.invariantId} has coverage evidence.` };
|
|
134
237
|
}
|