@ecoma-io/archkeep 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +156 -66
- package/gate-attestation.mjs +23 -0
- package/package.json +3 -1
- package/src/analysis/analyze.mjs +6 -0
- package/src/analysis/contract.md +32 -5
- package/src/analysis/csharp.mjs +18 -0
- package/src/analysis/go.mjs +18 -0
- package/src/analysis/java.mjs +15 -0
- package/src/analysis/kotlin.mjs +15 -0
- package/src/analysis/python.mjs +25 -3
- package/src/analysis/rust.mjs +18 -0
- package/src/analysis/source-util.mjs +113 -0
- package/src/analysis/typescript.mjs +86 -5
- package/src/canonical.mjs +43 -25
- package/src/commands/README.md +63 -12
- package/src/commands/change-intent.mjs +25 -1
- package/src/commands/change.mjs +90 -40
- package/src/commands/check.mjs +65 -26
- package/src/commands/completeness.mjs +126 -19
- package/src/commands/context-command.mjs +13 -5
- package/src/commands/context.mjs +31 -4
- package/src/commands/coverage-verdict.mjs +191 -0
- package/src/commands/debt.mjs +18 -15
- package/src/commands/delta-classify.mjs +13 -18
- package/src/commands/delta-snapshot.mjs +13 -5
- package/src/commands/delta.mjs +95 -33
- package/src/commands/diff.mjs +31 -24
- package/src/commands/discover.mjs +70 -29
- package/src/commands/drift.mjs +21 -21
- package/src/commands/edge-constraints.mjs +47 -1
- package/src/commands/evaluation-primitives.mjs +194 -2
- package/src/commands/evolution.mjs +27 -10
- package/src/commands/explain.mjs +14 -13
- package/src/commands/fitness.mjs +20 -19
- package/src/commands/graph.mjs +29 -11
- package/src/commands/health.mjs +12 -5
- package/src/commands/history.mjs +41 -26
- package/src/commands/impact.mjs +17 -18
- package/src/commands/plan-context-command.mjs +10 -5
- package/src/commands/reconcile.mjs +14 -17
- package/src/commands/scenario-evaluation.mjs +93 -16
- package/src/commands/scenario.mjs +28 -18
- package/src/commands/waivers.mjs +36 -28
- package/src/governance/evolution-event.mjs +96 -9
- package/src/intent/intent-manifest.json +83 -39
- package/src/lsp/diagnose.mjs +12 -3
- package/src/report/discover-text.mjs +31 -9
- package/src/report/graph-text.mjs +25 -5
- package/src/report/json.mjs +32 -5
- package/src/report/text.mjs +82 -12
- package/src/verdict.mjs +78 -36
- package/src/verify-gate-attestation.mjs +323 -0
- package/src/workspace.mjs +126 -2
package/src/report/json.mjs
CHANGED
|
@@ -94,12 +94,39 @@ export function jsonEnvelope({ command, context, status, exitCode, coverage, res
|
|
|
94
94
|
`them disagree would make one of them a lie.`,
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
// Completeness law, one-directional (#595, #599): `complete: true`
|
|
98
|
+
// claims the run judged everything its coverage block describes, and a
|
|
99
|
+
// whole-file failure or an unresolvable import site is exactly the kind
|
|
100
|
+
// of thing it did not judge — so the claim is refused over either. The
|
|
101
|
+
// reverse is deliberately allowed: `complete: false` with both lists
|
|
102
|
+
// empty is the zero-analysis state (a scope that selected no owned file,
|
|
103
|
+
// in-scope files no analyzer claims), which names its reason in the
|
|
104
|
+
// envelope's status and decision rather than in these lists, and refusing
|
|
105
|
+
// it would leave a run that judged nothing unable to say so. blindSpots
|
|
106
|
+
// is optional because not every command's coverage carries it; where it
|
|
107
|
+
// is present, the law is enforced.
|
|
108
|
+
//
|
|
109
|
+
// Within blindSpots only the unresolvable-LITERAL class counts as unjudged
|
|
110
|
+
// work (#595, narrowed): a row carrying `dynamic: true` is the declared
|
|
111
|
+
// non-literal-import limit — the language itself saying the target is
|
|
112
|
+
// computed at runtime, unknowable to static analysis in principle — and a
|
|
113
|
+
// declared limit is disclosed, not withheld over. The classifier lives once
|
|
114
|
+
// in `analysis/source-util.mjs`; this guard reads the row field the same
|
|
115
|
+
// helper that built the rows set, so the two can never disagree.
|
|
116
|
+
const blindSpotList = Array.isArray(coverage.blindSpots) ? coverage.blindSpots : [];
|
|
117
|
+
const unjudgedBlindSpots = blindSpotList.filter(
|
|
118
|
+
(row) => row.dynamic !== true && row.external !== true,
|
|
119
|
+
);
|
|
120
|
+
if (
|
|
121
|
+
coverage.complete === true &&
|
|
122
|
+
(coverage.notAnalyzed.length > 0 || unjudgedBlindSpots.length > 0)
|
|
123
|
+
) {
|
|
98
124
|
throw new Error(
|
|
99
|
-
`archkeep: refusing to build a JSON envelope
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
`
|
|
125
|
+
`archkeep: refusing to build a JSON envelope claiming coverage.complete (${coverage.complete}) ` +
|
|
126
|
+
`over unjudged work (notAnalyzed: ${coverage.notAnalyzed.length} entr${coverage.notAnalyzed.length === 1 ? "y" : "ies"}, ` +
|
|
127
|
+
`blindSpots: ${unjudgedBlindSpots.length}) — a run ` +
|
|
128
|
+
`that could not read everything cannot claim to have. This is a bug in the command that built ` +
|
|
129
|
+
`this envelope, not a fact about the workspace being judged.`,
|
|
103
130
|
);
|
|
104
131
|
}
|
|
105
132
|
// A bare `null` decision is the same programming error as a hand-built
|
package/src/report/text.mjs
CHANGED
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
* wearing a formatter's name, and it would disagree with the engine the first
|
|
22
22
|
* time either changed (`README.md` beside this file).
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
isDynamicSiteFailure,
|
|
26
|
+
isExternalSiteFailure,
|
|
27
|
+
isWholeFileFailure,
|
|
28
|
+
} from "../analysis/source-util.mjs";
|
|
25
29
|
|
|
26
30
|
/** Two spaces of indent for a violation's detail lines, four for wrapped text. */
|
|
27
31
|
const DETAIL = " ";
|
|
@@ -128,12 +132,19 @@ const formatFailure = (failure) =>
|
|
|
128
132
|
* consequences and one heading for both hid that for as long as it existed.
|
|
129
133
|
*
|
|
130
134
|
* A SITE failure is a blind spot: the file was analyzed, and one specifier in
|
|
131
|
-
* it is not statically knowable
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* it is not statically knowable. Three classes, and the section says which
|
|
136
|
+
* class did what instead of leaving the exit code to be discovered. An
|
|
137
|
+
* unresolvable literal referencing the workspace's own surface — path-like,
|
|
138
|
+
* `#` subpath, `paths` alias — is a concrete question the resolver was asked
|
|
139
|
+
* about the governed graph and could not answer: it WITHHELD the verdict
|
|
140
|
+
* (exit 3 on a findings-free run, #595). An unresolvable bare-package
|
|
141
|
+
* specifier names no project the workspace declares — `external: true`, the
|
|
142
|
+
* resolvability question an installed dependency tree answers, which a
|
|
143
|
+
* workspace legitimately may not have (the native self-check's `git archive`
|
|
144
|
+
* copy is the measured case: 284 rows) — disclosed without withholding. A
|
|
145
|
+
* non-literal `import()`/`require()` argument is the language declaring the
|
|
146
|
+
* target computed at runtime — a declared limit, `dynamic: true`, disclosed
|
|
147
|
+
* without withholding.
|
|
137
148
|
*
|
|
138
149
|
* A WHOLE-FILE failure is a hole: nothing was read, parsed, or analyzed — or a
|
|
139
150
|
* literal import that names a DECLARED project could not be resolved, so the
|
|
@@ -165,12 +176,42 @@ export function formatFailures(failures) {
|
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
if (blind.length > 0) {
|
|
179
|
+
const dyn = blind.filter(isDynamicSiteFailure).length;
|
|
180
|
+
const ext = blind.filter(isExternalSiteFailure).length;
|
|
181
|
+
const literal = blind.length - dyn - ext;
|
|
168
182
|
sections.push(
|
|
169
183
|
[
|
|
170
|
-
`${blind.length} import${blind.length === 1 ? "" : "s"} could not be resolved
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
`${blind.length} import${blind.length === 1 ? "" : "s"} could not be resolved — ` +
|
|
185
|
+
`blind spots inside files that were analyzed:`,
|
|
186
|
+
// All permanent classes are disclosed; the verdict treats them
|
|
187
|
+
// differently, and the report says which class did what instead of
|
|
188
|
+
// leaving the exit code to be discovered (the same posture the
|
|
189
|
+
// whole-file section's heading holds).
|
|
190
|
+
...(literal > 0
|
|
191
|
+
? [
|
|
192
|
+
`${literal} unresolvable literal import${literal === 1 ? "" : "s"} withheld the run's verdict (#595):`,
|
|
193
|
+
]
|
|
194
|
+
: []),
|
|
195
|
+
...blind
|
|
196
|
+
.filter((failure) => !isDynamicSiteFailure(failure) && !isExternalSiteFailure(failure))
|
|
197
|
+
.map(formatFailure),
|
|
198
|
+
...(ext > 0
|
|
199
|
+
? [
|
|
200
|
+
`${ext} unresolvable package import${ext === 1 ? "" : "s"} name${ext === 1 ? "s" : ""} ` +
|
|
201
|
+
`no project the workspace declares — external, disclosed without withholding:`,
|
|
202
|
+
]
|
|
203
|
+
: []),
|
|
204
|
+
...blind.filter(isExternalSiteFailure).map(formatFailure),
|
|
205
|
+
...(dyn > 0
|
|
206
|
+
? [
|
|
207
|
+
`${dyn} non-literal import() argument${dyn === 1 ? "" : "s"} — a declared limit static analysis cannot answer; ` +
|
|
208
|
+
`the verdict stands over the statically judgeable surface:`,
|
|
209
|
+
]
|
|
210
|
+
: []),
|
|
211
|
+
...blind.filter(isDynamicSiteFailure).map(formatFailure),
|
|
212
|
+
]
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
.join("\n"),
|
|
174
215
|
);
|
|
175
216
|
}
|
|
176
217
|
return sections.join("\n\n");
|
|
@@ -751,6 +792,27 @@ export function formatAcceptedViolations(waived, unresolvedDecisionRefs) {
|
|
|
751
792
|
].join("\n\n");
|
|
752
793
|
}
|
|
753
794
|
|
|
795
|
+
/**
|
|
796
|
+
* The could-not-look section: the reasons a run that found no violation is
|
|
797
|
+
* still not a pass, spelled the same way the JSON envelope words them
|
|
798
|
+
* (`../verdict.mjs`'s `coverageIncompleteReasons`, joined into
|
|
799
|
+
* `decision.reason`).
|
|
800
|
+
*
|
|
801
|
+
* "✔ no boundary violations" states only the boundary half of the verdict —
|
|
802
|
+
* on a run whose exit is 3, the reader needs the next lines to say WHY the
|
|
803
|
+
* run failed despite it, or the checkmark reads as a clean tree (P1-04's
|
|
804
|
+
* repro: an exit-3 run printed the checkmark and nothing else). Empty exactly
|
|
805
|
+
* when the run reached a verdict on everything it looked at.
|
|
806
|
+
*
|
|
807
|
+
* @param {string[]} coverageIncomplete The verdict's reason clauses, in
|
|
808
|
+
* `verdictFor`'s pinned order.
|
|
809
|
+
* @returns {string} Empty exactly when there is nothing to disclose.
|
|
810
|
+
*/
|
|
811
|
+
export function formatCoverageIncomplete(coverageIncomplete) {
|
|
812
|
+
if (coverageIncomplete.length === 0) return "";
|
|
813
|
+
return coverageIncomplete.map((reason) => `⚠ ${reason}`).join("\n");
|
|
814
|
+
}
|
|
815
|
+
|
|
754
816
|
/**
|
|
755
817
|
* The whole report, violations first.
|
|
756
818
|
*
|
|
@@ -766,7 +828,7 @@ export function formatAcceptedViolations(waived, unresolvedDecisionRefs) {
|
|
|
766
828
|
* summary line above only says "no boundary violations" when there is nothing
|
|
767
829
|
* a waiver is covering either.
|
|
768
830
|
*
|
|
769
|
-
* @param {{violations: object[], failures: object[], analyzed: number, projects: number, imports: number, goWork?: object|null, tsconfigPaths?: object|null, declaredEdges?: object|null, intent?: object|null, fitness?: object|null, fitnessOverall?: {verdict: string}|null, customRules?: {decisions: object[], overall: {verdict: string}}|null, coverageGaps?: object[], notes?: string[], policy?: {profile: string|null, source: string, fingerprint: string}|null, unresolvedDecisionRefs?: Set<string
|
|
831
|
+
* @param {{violations: object[], failures: object[], analyzed: number, projects: number, imports: number, goWork?: object|null, tsconfigPaths?: object|null, declaredEdges?: object|null, intent?: object|null, fitness?: object|null, fitnessOverall?: {verdict: string}|null, customRules?: {decisions: object[], overall: {verdict: string}}|null, coverageGaps?: object[], notes?: string[], policy?: {profile: string|null, source: string, fingerprint: string}|null, unresolvedDecisionRefs?: Set<string>, coverageIncomplete?: string[]}} run
|
|
770
832
|
* @returns {string}
|
|
771
833
|
*/
|
|
772
834
|
export function formatReport({
|
|
@@ -786,6 +848,7 @@ export function formatReport({
|
|
|
786
848
|
notes = [],
|
|
787
849
|
policy = null,
|
|
788
850
|
unresolvedDecisionRefs,
|
|
851
|
+
coverageIncomplete = [],
|
|
789
852
|
}) {
|
|
790
853
|
const inspected =
|
|
791
854
|
`${imports} import${imports === 1 ? "" : "s"} in ${analyzed} file${analyzed === 1 ? "" : "s"} ` +
|
|
@@ -829,6 +892,13 @@ export function formatReport({
|
|
|
829
892
|
}
|
|
830
893
|
}
|
|
831
894
|
|
|
895
|
+
// The could-not-look reasons render BEFORE the per-feature sections, right
|
|
896
|
+
// after the verdict summary they qualify: a reader who just read "✔ no
|
|
897
|
+
// boundary violations" needs the next lines to be the reason the exit is
|
|
898
|
+
// still 3, not go.work bookkeeping.
|
|
899
|
+
const coverageIncompleteSection = formatCoverageIncomplete(coverageIncomplete);
|
|
900
|
+
if (coverageIncompleteSection !== "") sections.push(coverageIncompleteSection);
|
|
901
|
+
|
|
832
902
|
const goWorkSection = formatGoWork(goWork);
|
|
833
903
|
if (goWorkSection !== "") sections.push(goWorkSection);
|
|
834
904
|
|
package/src/verdict.mjs
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Both sit here rather than in `../cli.mjs` because two callers need them and
|
|
6
6
|
* only one of the two is the CLI shell: `./commands/check.mjs` words its own
|
|
7
|
-
* `--format json` envelope from `verdictFor`, and
|
|
8
|
-
* takes the process's exit code from the same call.
|
|
9
|
-
* `EXIT` under its own name, so every importer that
|
|
10
|
-
* there keeps working.
|
|
7
|
+
* `--format json` envelope's `status` and `exitCode` from `verdictFor`, and
|
|
8
|
+
* `../cli.mjs`'s `runCheck` takes the process's exit code from the same call.
|
|
9
|
+
* `../cli.mjs` re-exports `EXIT` under its own name, so every importer that
|
|
10
|
+
* already reads it from there keeps working.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { buildDecision } from "./report/evidence.mjs";
|
|
@@ -18,6 +18,29 @@ export const EXIT = Object.freeze({
|
|
|
18
18
|
usage: 2,
|
|
19
19
|
error: 3,
|
|
20
20
|
});
|
|
21
|
+
/**
|
|
22
|
+
* The coverage clauses of a no-verdict reason, spelled once — the strings
|
|
23
|
+
* `verdictFor` joins into `decision.reason` and `check`'s text report renders
|
|
24
|
+
* beside its headline, so the two faces cannot disagree about WHY a run
|
|
25
|
+
* failed to reach a verdict. The clauses cover only the three coverage axes
|
|
26
|
+
* (whole-file failures, unresolved sites, zero analysis); the intent,
|
|
27
|
+
* fitness and custom-rule clauses stay local to `verdictFor` because they
|
|
28
|
+
* name verdict surfaces the coverage counts cannot see.
|
|
29
|
+
*
|
|
30
|
+
* @param {{unchecked: number, blindSpots: number, analyzed: number}} counts
|
|
31
|
+
* @returns {string[]} One clause per failed coverage axis, in pinned order.
|
|
32
|
+
*/
|
|
33
|
+
export function coverageIncompleteReasons({ unchecked, blindSpots, analyzed }) {
|
|
34
|
+
return [
|
|
35
|
+
unchecked > 0
|
|
36
|
+
? `${unchecked} file${unchecked === 1 ? "" : "s"} could not be analyzed — coverage incomplete`
|
|
37
|
+
: null,
|
|
38
|
+
blindSpots > 0
|
|
39
|
+
? `${blindSpots} import site${blindSpots === 1 ? "" : "s"} could not be resolved — coverage incomplete`
|
|
40
|
+
: null,
|
|
41
|
+
analyzed === 0 ? "no file in scope could be analyzed — coverage incomplete" : null,
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
}
|
|
21
44
|
/**
|
|
22
45
|
* The one place that turns a run's counts into the verdict every format
|
|
23
46
|
* agrees on. `runCheck` uses it for the process's exit code; `check` uses the
|
|
@@ -41,8 +64,12 @@ export const EXIT = Object.freeze({
|
|
|
41
64
|
* with no findings), which makes a regression in this mapping a loud error
|
|
42
65
|
* rather than a silent one.
|
|
43
66
|
*
|
|
44
|
-
* @param {{violations: number, declaredEdgeFindings: number, goWorkDrift: number, tsconfigPathsDead: number, intentFindings: number, intentUnresolved: number, intentUnresolvedDecisionRefs?: number, unchecked: number, fitnessFail?: number, fitnessUnknown?: number, customRuleFail?: number, customRuleUnknown?: number}} counts
|
|
45
|
-
* @returns {{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3, decision: object}}
|
|
67
|
+
* @param {{violations: number, declaredEdgeFindings: number, goWorkDrift: number, tsconfigPathsDead: number, intentFindings: number, intentUnresolved: number, intentUnresolvedDecisionRefs?: number, unchecked: number, analyzed: number, blindSpots: number, fitnessFail?: number, fitnessUnknown?: number, customRuleFail?: number, customRuleUnknown?: number}} counts
|
|
68
|
+
* @returns {{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3, reasons: string[], decision: object}}
|
|
69
|
+
* `reasons` is the coverage clause list behind this verdict — the
|
|
70
|
+
* could-not-look clauses on the findings and no-verdict lanes, empty on the
|
|
71
|
+
* clean one. `decision.reason` joins it with the intent/fitness/custom
|
|
72
|
+
* clauses where the lane is no-verdict.
|
|
46
73
|
*/
|
|
47
74
|
export function verdictFor({
|
|
48
75
|
violations,
|
|
@@ -53,11 +80,14 @@ export function verdictFor({
|
|
|
53
80
|
intentUnresolved,
|
|
54
81
|
intentUnresolvedDecisionRefs = 0,
|
|
55
82
|
unchecked,
|
|
83
|
+
analyzed,
|
|
84
|
+
blindSpots,
|
|
56
85
|
fitnessFail = 0,
|
|
57
86
|
fitnessUnknown = 0,
|
|
58
87
|
customRuleFail = 0,
|
|
59
88
|
customRuleUnknown = 0,
|
|
60
89
|
}) {
|
|
90
|
+
const coverageReasons = coverageIncompleteReasons({ unchecked, blindSpots, analyzed });
|
|
61
91
|
if (
|
|
62
92
|
violations > 0 ||
|
|
63
93
|
declaredEdgeFindings > 0 ||
|
|
@@ -74,9 +104,10 @@ export function verdictFor({
|
|
|
74
104
|
return {
|
|
75
105
|
status: "findings",
|
|
76
106
|
exitCode: EXIT.violations,
|
|
107
|
+
reasons: coverageReasons,
|
|
77
108
|
decision: buildDecision({
|
|
78
109
|
status: "findings",
|
|
79
|
-
coverageComplete: unchecked === 0,
|
|
110
|
+
coverageComplete: unchecked === 0 && blindSpots === 0 && analyzed > 0,
|
|
80
111
|
findings:
|
|
81
112
|
violations +
|
|
82
113
|
declaredEdgeFindings +
|
|
@@ -90,52 +121,63 @@ export function verdictFor({
|
|
|
90
121
|
}
|
|
91
122
|
if (
|
|
92
123
|
unchecked > 0 ||
|
|
124
|
+
// An unresolvable site was seen but never judged (#595): named in
|
|
125
|
+
// coverage.blindSpots, and echoed here so the exit says it too — a
|
|
126
|
+
// pass over a site the run could not read claims a verdict it does
|
|
127
|
+
// not hold.
|
|
128
|
+
blindSpots > 0 ||
|
|
129
|
+
// A run that analyzed nothing judged nothing (#599) — a scope that
|
|
130
|
+
// selected no project-owned file, or in-scope files no analyzer
|
|
131
|
+
// claims. Judging nothing is not finding nothing.
|
|
132
|
+
analyzed === 0 ||
|
|
93
133
|
intentUnresolved > 0 ||
|
|
94
134
|
intentUnresolvedDecisionRefs > 0 ||
|
|
95
135
|
fitnessUnknown > 0 ||
|
|
96
136
|
customRuleUnknown > 0
|
|
97
137
|
) {
|
|
138
|
+
// The list is built before the return so `decision.reason` joins the very
|
|
139
|
+
// array the envelope's `reasons` carries — one list, two renderings, and
|
|
140
|
+
// neither can drift from the other.
|
|
141
|
+
const reasons = [
|
|
142
|
+
...coverageReasons,
|
|
143
|
+
// The could-not-look condition, named so a reader knows WHICH half of
|
|
144
|
+
// the run did not reach a verdict (I3). When read-only coverage and
|
|
145
|
+
// intent both failed, name both — a reason naming only the file count
|
|
146
|
+
// would hide the unresolved intent boundary from a reader acting on
|
|
147
|
+
// the reason alone (it stays visible in result.intent.unresolved, and
|
|
148
|
+
// status is still no-verdict, so nothing is silent). Each clause below
|
|
149
|
+
// is independent of the others — none is gated on a sibling clause
|
|
150
|
+
// being zero — so a tree that fails on several axes at once names
|
|
151
|
+
// every one of them, not only the first the array happens to hit.
|
|
152
|
+
intentUnresolved > 0
|
|
153
|
+
? `${intentUnresolved} architecture-intent boundary or row${intentUnresolved === 1 ? "" : "s"} could not be established`
|
|
154
|
+
: null,
|
|
155
|
+
intentUnresolvedDecisionRefs > 0
|
|
156
|
+
? `${intentUnresolvedDecisionRefs} intent row${intentUnresolvedDecisionRefs === 1 ? "" : "s"} ${intentUnresolvedDecisionRefs === 1 ? "cites" : "cite"} a decisionRef that does not resolve`
|
|
157
|
+
: null,
|
|
158
|
+
fitnessUnknown > 0
|
|
159
|
+
? `${fitnessUnknown} fitness functions${fitnessUnknown === 1 ? "" : "s"} could not be determined`
|
|
160
|
+
: null,
|
|
161
|
+
customRuleUnknown > 0
|
|
162
|
+
? `${customRuleUnknown} custom rule${customRuleUnknown === 1 ? "" : "s"} could not be judged`
|
|
163
|
+
: null,
|
|
164
|
+
].filter(Boolean);
|
|
98
165
|
return {
|
|
99
166
|
status: "no-verdict",
|
|
100
167
|
exitCode: EXIT.error,
|
|
168
|
+
reasons,
|
|
101
169
|
decision: buildDecision({
|
|
102
170
|
status: "no-verdict",
|
|
103
|
-
coverageComplete: unchecked === 0,
|
|
171
|
+
coverageComplete: unchecked === 0 && blindSpots === 0 && analyzed > 0,
|
|
104
172
|
findings: 0,
|
|
105
|
-
|
|
106
|
-
// the run did not reach a verdict (I3). When read-only coverage and
|
|
107
|
-
// intent both failed, name both — a reason naming only the file count
|
|
108
|
-
// would hide the unresolved intent boundary from a reader acting on
|
|
109
|
-
// the reason alone (it stays visible in result.intent.unresolved, and
|
|
110
|
-
// status is still no-verdict, so nothing is silent). Each clause below
|
|
111
|
-
// is independent of the others — none is gated on a sibling clause
|
|
112
|
-
// being zero — so a tree that fails on several axes at once names
|
|
113
|
-
// every one of them, not just the first the array happens to check.
|
|
114
|
-
reason: [
|
|
115
|
-
unchecked > 0
|
|
116
|
-
? `${unchecked} file${unchecked === 1 ? "" : "s"} could not be analyzed — coverage incomplete`
|
|
117
|
-
: null,
|
|
118
|
-
intentUnresolved > 0
|
|
119
|
-
? `${intentUnresolved} architecture-intent boundary or row${intentUnresolved === 1 ? "" : "s"} could not be established`
|
|
120
|
-
: null,
|
|
121
|
-
intentUnresolvedDecisionRefs > 0
|
|
122
|
-
? `${intentUnresolvedDecisionRefs} intent row${intentUnresolvedDecisionRefs === 1 ? "" : "s"} ${intentUnresolvedDecisionRefs === 1 ? "cites" : "cite"} a decisionRef that does not resolve`
|
|
123
|
-
: null,
|
|
124
|
-
fitnessUnknown > 0
|
|
125
|
-
? `${fitnessUnknown} fitness function${fitnessUnknown === 1 ? "" : "s"} could not be determined`
|
|
126
|
-
: null,
|
|
127
|
-
customRuleUnknown > 0
|
|
128
|
-
? `${customRuleUnknown} custom rule${customRuleUnknown === 1 ? "" : "s"} could not be judged`
|
|
129
|
-
: null,
|
|
130
|
-
]
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
.join("; "),
|
|
173
|
+
reason: reasons.join("; "),
|
|
133
174
|
}),
|
|
134
175
|
};
|
|
135
176
|
}
|
|
136
177
|
return {
|
|
137
178
|
status: "ok",
|
|
138
179
|
exitCode: EXIT.ok,
|
|
180
|
+
reasons: [],
|
|
139
181
|
decision: buildDecision({
|
|
140
182
|
status: "ok",
|
|
141
183
|
coverageComplete: true,
|