@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/commands/impact.mjs
CHANGED
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
* under project roots, `impact` refuses loudly rather than returning a result
|
|
36
36
|
* whose dependents silently under-represent the real architecture.
|
|
37
37
|
*/
|
|
38
|
-
import { isWholeFileFailure } from "../analysis/source-util.mjs";
|
|
39
38
|
import { UsageError } from "../errors.mjs";
|
|
40
39
|
import { computeImpactConstraints } from "./edge-constraints.mjs";
|
|
40
|
+
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
41
41
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
42
42
|
import { formatImpactReport } from "../report/impact-text.mjs";
|
|
43
43
|
import { resolveProvenance } from "./provenance.mjs";
|
|
@@ -123,11 +123,14 @@ export function computeImpact(projectName, graph) {
|
|
|
123
123
|
* @param {object} commandContext From `resolveCommandContext`.
|
|
124
124
|
* @param {object} [config] The loaded boundary config. When provided,
|
|
125
125
|
* constraint context and violations for each dependent edge are computed.
|
|
126
|
-
* @returns {{status: "ok"|"no-verdict", impact
|
|
126
|
+
* @returns {{status: "ok"|"no-verdict", impact?: object, coverage: object,
|
|
127
127
|
* report: {text: string, json: string}}}
|
|
128
|
+
* `status: "no-verdict"` carries no `impact` payload — the verdict was
|
|
129
|
+
* withheld, and the envelope's `coverage` block is the whole answer (#608).
|
|
128
130
|
* @throws {Error} when an Nx workspace has polyglot manifests but the plugin
|
|
129
|
-
* is not registered, or when the named project does not exist in the graph
|
|
130
|
-
*
|
|
131
|
+
* is not registered, or when the named project does not exist in the graph.
|
|
132
|
+
* Incomplete coverage returns the structured no-verdict envelope instead of
|
|
133
|
+
* throwing (#608).
|
|
131
134
|
*/
|
|
132
135
|
export function impactCommand(projectName, commandContext, config = null) {
|
|
133
136
|
const { root, provider, marker, graph, pluginGap } = commandContext;
|
|
@@ -150,22 +153,18 @@ export function impactCommand(projectName, commandContext, config = null) {
|
|
|
150
153
|
// project before the run invests in anything else.
|
|
151
154
|
const impact = computeImpact(projectName, graph);
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
`under-represent the real architecture. Fix the unanalyzed files and re-run.`,
|
|
162
|
-
);
|
|
156
|
+
// The impact set is a claim about the tree the run read, refused through the
|
|
157
|
+
// one structured contract `./coverage-verdict.mjs` builds (#608): the
|
|
158
|
+
// verdict is withheld in-band — status "no-verdict", exit 3, a `coverage`
|
|
159
|
+
// block naming every file and site the run could not judge — where a parser
|
|
160
|
+
// and `--output` can read it, not on stderr where only a human can.
|
|
161
|
+
const completeness = coverageVerdict(commandContext);
|
|
162
|
+
if (!completeness.complete) {
|
|
163
|
+
return coverageRefusal({ command: "impact", commandContext, what: "computing impact" });
|
|
163
164
|
}
|
|
164
|
-
const blindSpots =
|
|
165
|
-
.filter((f) => !isWholeFileFailure(f))
|
|
166
|
-
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason }));
|
|
165
|
+
const blindSpots = completeness.blindSpots;
|
|
167
166
|
|
|
168
|
-
const complete = true; //
|
|
167
|
+
const complete = true; // the incompleteness cases all returned above
|
|
169
168
|
const status = "ok";
|
|
170
169
|
const exitCode = 0;
|
|
171
170
|
|
|
@@ -64,7 +64,11 @@ import { statSync } from "node:fs";
|
|
|
64
64
|
import { join } from "node:path";
|
|
65
65
|
|
|
66
66
|
import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
67
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
blindSpotRows,
|
|
69
|
+
isWholeFileFailure,
|
|
70
|
+
unresolvableLiteralCount,
|
|
71
|
+
} from "../analysis/source-util.mjs";
|
|
68
72
|
import { tsconfigPathsFacts } from "../analysis/typescript.mjs";
|
|
69
73
|
import { compareGoWork, parseGoWorkUse } from "../go-work.mjs";
|
|
70
74
|
import { judgeTsconfigPaths } from "../tsconfig-paths.mjs";
|
|
@@ -497,7 +501,10 @@ export async function planContextCommand(
|
|
|
497
501
|
(a.messageId < b.messageId ? -1 : a.messageId > b.messageId ? 1 : 0),
|
|
498
502
|
);
|
|
499
503
|
|
|
500
|
-
|
|
504
|
+
// An unresolvable literal site is work the run saw but never judged
|
|
505
|
+
// (#595, narrowed): a plan over it would present edges the run does not
|
|
506
|
+
// hold, so it defeats completeness the way a whole-file failure does.
|
|
507
|
+
const complete = notAnalyzed.length === 0 && unresolvableLiteralCount(failures) === 0;
|
|
501
508
|
const status = complete ? "ok" : "no-verdict";
|
|
502
509
|
const exitCode = complete ? 0 : 3;
|
|
503
510
|
|
|
@@ -525,9 +532,7 @@ export async function planContextCommand(
|
|
|
525
532
|
analyzedFiles: wholeTree.analyzed,
|
|
526
533
|
imports: wholeTree.imports.length,
|
|
527
534
|
notAnalyzed,
|
|
528
|
-
blindSpots: failures
|
|
529
|
-
.filter((failure) => !isWholeFileFailure(failure))
|
|
530
|
-
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
535
|
+
blindSpots: blindSpotRows(failures),
|
|
531
536
|
// Intent's own coverage notes ride the same seam `check` threads them on
|
|
532
537
|
// (today only an `"optional": true` allowed row whose statement is absent),
|
|
533
538
|
// so the plan's text and JSON reports read the same notes `check` does.
|
|
@@ -43,9 +43,10 @@
|
|
|
43
43
|
* comparison (never `localeCompare`), so two runs over an unchanged tree and
|
|
44
44
|
* intent produce byte-identical text and JSON.
|
|
45
45
|
*/
|
|
46
|
-
import {
|
|
46
|
+
import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
47
47
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
48
48
|
import { resolveProvenance } from "./provenance.mjs";
|
|
49
|
+
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
49
50
|
import { judgeIntent } from "../architecture-intent/judge.mjs";
|
|
50
51
|
import { computeIntentFingerprint } from "../architecture-intent/intent-fingerprint.mjs";
|
|
51
52
|
import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
@@ -84,9 +85,12 @@ function intentRows(intent) {
|
|
|
84
85
|
* @param {{loadIntentOverride?: (root: string) => Promise<object>}} [io]
|
|
85
86
|
* Injectable intent loader for tests.
|
|
86
87
|
* @param {{propose?: boolean}} [options] `--propose` adds the ranked candidate list.
|
|
87
|
-
* @returns {Promise<{status: "ok", reconcile
|
|
88
|
+
* @returns {Promise<{status: "ok"|"no-verdict", reconcile?: object, coverage: object,
|
|
88
89
|
* report: {text: string, json: string}}>}
|
|
89
|
-
*
|
|
90
|
+
* `status: "no-verdict"` carries no `reconcile` payload — the verdict was
|
|
91
|
+
* withheld, and the envelope's `coverage` block is the whole answer (#608).
|
|
92
|
+
* @throws {Error} on every condition the header lists except the coverage one,
|
|
93
|
+
* which returns instead of throwing.
|
|
90
94
|
*/
|
|
91
95
|
export async function reconcileCommand(commandContext, io = {}, options = {}) {
|
|
92
96
|
const { root, provider, marker, analysis } = commandContext;
|
|
@@ -94,17 +98,12 @@ export async function reconcileCommand(commandContext, io = {}, options = {}) {
|
|
|
94
98
|
refuseIncompleteGraph(commandContext);
|
|
95
99
|
|
|
96
100
|
// A reconcile verdict cannot be established over a tree it could not fully
|
|
97
|
-
// read — the same fail-closed condition `drift` enforces
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
throw new Error(
|
|
104
|
-
`archkeep: reconcile has incomplete coverage — ${notAnalyzed.length} file` +
|
|
105
|
-
`${notAnalyzed.length === 1 ? "" : "s"} could not be analyzed, so every "absent" score ` +
|
|
106
|
-
`would be ambiguous between "gone" and "never seen". Fix the unanalyzed files and re-run.`,
|
|
107
|
-
);
|
|
101
|
+
// read — the same fail-closed condition `drift` enforces, refused through
|
|
102
|
+
// the same structured envelope `./coverage-verdict.mjs` builds (#608): the
|
|
103
|
+
// verdict is withheld in-band, where a parser and `--output` can read it.
|
|
104
|
+
const completeness = coverageVerdict(commandContext);
|
|
105
|
+
if (!completeness.complete) {
|
|
106
|
+
return coverageRefusal({ command: "reconcile", commandContext, what: "reconciling" });
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
const intent = await (io.loadIntentOverride ?? loadIntent)(root, {
|
|
@@ -146,9 +145,7 @@ export async function reconcileCommand(commandContext, io = {}, options = {}) {
|
|
|
146
145
|
// Reconcile reads only the graph — provider failures are the same blind
|
|
147
146
|
// spots every other command reports, and a blind spot never prevents a
|
|
148
147
|
// verdict.
|
|
149
|
-
blindSpots: analysis.failures
|
|
150
|
-
.filter((failure) => !isWholeFileFailure(failure))
|
|
151
|
-
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
148
|
+
blindSpots: blindSpotRows(analysis.failures),
|
|
152
149
|
// Coverage notes (e.g. an `optional: true` allowed row the team has not
|
|
153
150
|
// built yet) ride here so "optional and absent" never reads as "never
|
|
154
151
|
// checked".
|
|
@@ -33,12 +33,21 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { computeImpact } from "./impact.mjs";
|
|
35
35
|
import { computeImpactConstraints } from "./edge-constraints.mjs";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
buildDecisionImpact,
|
|
38
|
+
buildEvolutionAlignment,
|
|
39
|
+
decisionProvenanceCoverage,
|
|
40
|
+
} from "./evaluation-primitives.mjs";
|
|
37
41
|
import { resolveProvenance } from "./provenance.mjs";
|
|
38
42
|
import {
|
|
39
|
-
buildScenarioCompleteness,
|
|
40
43
|
buildGovernanceCompleteness,
|
|
44
|
+
buildScenarioCompleteness,
|
|
45
|
+
buildEvidenceComplete,
|
|
46
|
+
createDomain,
|
|
47
|
+
EVALUATED,
|
|
48
|
+
NOT_EVALUATED,
|
|
41
49
|
evaluationStatus,
|
|
50
|
+
EVALUATION_CONTRACT_TYPES,
|
|
42
51
|
} from "./completeness.mjs";
|
|
43
52
|
|
|
44
53
|
// ---------------------------------------------------------------------------
|
|
@@ -105,8 +114,8 @@ export const SCENARIO_CHANGE_TYPES = Object.freeze(["dependency_added", "depende
|
|
|
105
114
|
* @property {string} evidenceChain.scenarioState The state after applying changes ("scenario").
|
|
106
115
|
* @property {object} evidenceChain.delta The computed differences.
|
|
107
116
|
* @property {object} [governanceImpact] Governance re-evaluation results.
|
|
108
|
-
* @property {boolean} governanceImpact.
|
|
109
|
-
* @property {boolean} governanceImpact.
|
|
117
|
+
* @property {boolean} governanceImpact.findingsFiltered Whether precomputed findings were filtered into the scenario state.
|
|
118
|
+
* @property {boolean} governanceImpact.debtFiltered Whether precomputed debt was filtered into the scenario state.
|
|
110
119
|
* @property {boolean} governanceImpact.governanceComplete Whether all governance data was provided.
|
|
111
120
|
* @property {number} governanceImpact.scenarioFindingsCount Number of findings in the scenario state.
|
|
112
121
|
* @property {number} governanceImpact.scenarioDebtCount Number of debt entries in the scenario state.
|
|
@@ -587,17 +596,19 @@ export function evaluateScenario(
|
|
|
587
596
|
// hypothetical graph) is NOT governance re-evaluation. True re-evaluation
|
|
588
597
|
// would run the full check pipeline against the hypothetical graph.
|
|
589
598
|
// When we only filter, governance is NOT_EVALUATED.
|
|
590
|
-
const
|
|
591
|
-
const
|
|
599
|
+
const findingsFiltered = availableFindings !== null;
|
|
600
|
+
const debtFiltered = availableDebt !== null;
|
|
592
601
|
|
|
593
|
-
//
|
|
602
|
+
// Filtering is NOT re-evaluation (the header above), so the status is
|
|
603
|
+
// NOT_EVALUATED on both paths — no re-evaluation pipeline exists to pass.
|
|
604
|
+
// Telling a consumer "evaluated" for a filter is the mislabel this refuses.
|
|
594
605
|
const findingsStatus = evaluationStatus({
|
|
595
|
-
evaluated: false,
|
|
596
|
-
notEvaluated:
|
|
606
|
+
evaluated: false,
|
|
607
|
+
notEvaluated: true,
|
|
597
608
|
});
|
|
598
609
|
const debtStatus = evaluationStatus({
|
|
599
|
-
evaluated: false,
|
|
600
|
-
notEvaluated:
|
|
610
|
+
evaluated: false,
|
|
611
|
+
notEvaluated: true,
|
|
601
612
|
});
|
|
602
613
|
|
|
603
614
|
// Build governance completeness
|
|
@@ -614,12 +625,78 @@ export function evaluateScenario(
|
|
|
614
625
|
const refusedCount = refused.length;
|
|
615
626
|
const mutationCoverageComplete = totalChanges === appliedCount && refusedCount === 0;
|
|
616
627
|
|
|
617
|
-
// Build
|
|
628
|
+
// Build the scenario's domain statuses FIRST — the Evidence-Complete
|
|
629
|
+
// contract below derives its hidden-gap gate from them, so a domain that
|
|
630
|
+
// is NOT_EVALUATED without a note flips that gate, and a domain that
|
|
631
|
+
// skips with a stated reason does not.
|
|
632
|
+
// structural — always evaluated (scenario builds a complete graph).
|
|
633
|
+
// constraint, boundary, decision — require a boundary config; decision
|
|
634
|
+
// follows the config exactly as the canonical face reads it, because
|
|
635
|
+
// the scenario runs decision impact (buildDecisionImpact above) when
|
|
636
|
+
// one is present. The condition this replaces, `config.decisionRefs`,
|
|
637
|
+
// named a field no workspace can declare — it held the decision domain
|
|
638
|
+
// at NOT_EVALUATED forever, a permanent hidden gap on every configured
|
|
639
|
+
// workspace.
|
|
640
|
+
// findings, debt — never re-evaluated in a scenario; the notes say so,
|
|
641
|
+
// which is what keeps them disclosed and out of the hidden-gap count.
|
|
642
|
+
// evidence — always evaluated (we build the EC contract).
|
|
643
|
+
const hasConfig = config !== null;
|
|
644
|
+
const configGapNote =
|
|
645
|
+
"No boundary config — constraint, boundary and decision rules not evaluated";
|
|
646
|
+
const configGated = () =>
|
|
647
|
+
hasConfig ? createDomain(EVALUATED) : createDomain(NOT_EVALUATED, configGapNote);
|
|
648
|
+
const scenarioDomains = {
|
|
649
|
+
structural: createDomain(EVALUATED),
|
|
650
|
+
constraint: configGated(),
|
|
651
|
+
boundary: configGated(),
|
|
652
|
+
decision: configGated(),
|
|
653
|
+
findings:
|
|
654
|
+
governanceCompleteness.findings.status === NOT_EVALUATED
|
|
655
|
+
? createDomain(NOT_EVALUATED, "Findings not re-evaluated in scenario")
|
|
656
|
+
: governanceCompleteness.findings,
|
|
657
|
+
debt:
|
|
658
|
+
governanceCompleteness.debt.status === NOT_EVALUATED
|
|
659
|
+
? createDomain(NOT_EVALUATED, "Debt not re-evaluated in scenario")
|
|
660
|
+
: governanceCompleteness.debt,
|
|
661
|
+
evidence: createDomain(EVALUATED),
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// Hidden gaps: NOT_EVALUATED domains without a stated reason — the same
|
|
665
|
+
// derivation `deriveEvidenceGates` runs for the canonical face, so neither
|
|
666
|
+
// face can pass the gate on a literal while the other fails on facts.
|
|
667
|
+
let scenarioHiddenGapCount = 0;
|
|
668
|
+
for (const domainStatus of Object.values(scenarioDomains)) {
|
|
669
|
+
if (domainStatus.status === NOT_EVALUATED && !domainStatus.note) {
|
|
670
|
+
scenarioHiddenGapCount++;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Derive evidence gates for scenario evaluation and build Evidence-Complete contract.
|
|
675
|
+
// Scenario mutation is deterministic: same inputs → same outputs (pure graph clone + apply).
|
|
676
|
+
// surfaceParity: the scenario applied all requested changes (refused===0), so the
|
|
677
|
+
// hypothetical surface is internally consistent — no surface drift from the plan.
|
|
678
|
+
const surfaceParity = refusedCount === 0 ? 1 : 0;
|
|
679
|
+
|
|
680
|
+
const evidenceComplete = buildEvidenceComplete({
|
|
681
|
+
domainCoverage: currentDecisionImpact !== null ? 1 : 0,
|
|
682
|
+
claimEvidenceCoverage: config !== null ? 1 : 0,
|
|
683
|
+
causalCoverage: currentConstraintImpact !== null ? 1 : 0,
|
|
684
|
+
provenanceCoverage: decisionProvenanceCoverage(currentDecisionImpact?.decisions),
|
|
685
|
+
mutationCoverage: mutationCoverageComplete ? 1 : 0,
|
|
686
|
+
surfaceParity,
|
|
687
|
+
hiddenGapCount: scenarioHiddenGapCount,
|
|
688
|
+
falseCompleteCount: 0,
|
|
689
|
+
baseIdentityValid: base.identityVerified,
|
|
690
|
+
deterministic: true,
|
|
691
|
+
contractType: EVALUATION_CONTRACT_TYPES.SCENARIO,
|
|
692
|
+
});
|
|
618
693
|
const scenarioCompleteness = buildScenarioCompleteness({
|
|
619
694
|
changesComplete: mutationCoverageComplete,
|
|
620
695
|
baseIdentityVerified: base.identityVerified,
|
|
621
696
|
mutationCoverageComplete,
|
|
622
697
|
governance: governanceCompleteness,
|
|
698
|
+
evidenceComplete,
|
|
699
|
+
domains: scenarioDomains,
|
|
623
700
|
});
|
|
624
701
|
|
|
625
702
|
return {
|
|
@@ -655,9 +732,9 @@ export function evaluateScenario(
|
|
|
655
732
|
...(scenarioDebt !== null ? { debt: scenarioDebt } : {}),
|
|
656
733
|
},
|
|
657
734
|
governanceImpact: {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
governanceComplete: false,
|
|
735
|
+
findingsFiltered,
|
|
736
|
+
debtFiltered,
|
|
737
|
+
governanceComplete: false,
|
|
661
738
|
scenarioFindingsCount: scenarioFindings?.length ?? 0,
|
|
662
739
|
scenarioDebtCount: scenarioDebt?.length ?? 0,
|
|
663
740
|
findingsStatus,
|
|
@@ -665,7 +742,7 @@ export function evaluateScenario(
|
|
|
665
742
|
},
|
|
666
743
|
delta,
|
|
667
744
|
completeness: scenarioCompleteness,
|
|
668
|
-
complete:
|
|
745
|
+
complete: scenarioCompleteness.overallComplete,
|
|
669
746
|
notes,
|
|
670
747
|
};
|
|
671
748
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { resolveProvenance } from "./provenance.mjs";
|
|
13
13
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
15
15
|
import { evaluateScenario, parseScenarioInput } from "./scenario-evaluation.mjs";
|
|
16
16
|
export { parseScenarioInput } from "./scenario-evaluation.mjs";
|
|
17
17
|
|
|
@@ -23,7 +23,10 @@ export { parseScenarioInput } from "./scenario-evaluation.mjs";
|
|
|
23
23
|
* @param {string} scenarioJson The scenario description as JSON.
|
|
24
24
|
* @param {object} commandContext From `resolveCommandContext`.
|
|
25
25
|
* @param {object} [config] The loaded boundary config.
|
|
26
|
-
* @returns {{status:
|
|
26
|
+
* @returns {{status: "ok"|"no-verdict", scenario?: object, coverage: object,
|
|
27
|
+
* report: {text: string, json: string}}} `scenario` is absent under
|
|
28
|
+
* `status: "no-verdict"` — the coverage refusal (#608) withholds the
|
|
29
|
+
* evaluation, and the envelope's `coverage` block is the whole answer.
|
|
27
30
|
*/
|
|
28
31
|
export function scenarioCommand(projectName, scenarioJson, commandContext, config = null) {
|
|
29
32
|
const { root, provider, marker, graph, pluginGap } = commandContext;
|
|
@@ -44,17 +47,12 @@ export function scenarioCommand(projectName, scenarioJson, commandContext, confi
|
|
|
44
47
|
// Parse the scenario input
|
|
45
48
|
const scenarioInput = parseScenarioInput(scenarioJson);
|
|
46
49
|
|
|
47
|
-
// Check coverage
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
throw new Error(
|
|
54
|
-
`archkeep: the graph has incomplete coverage — ${notAnalyzed.length} file` +
|
|
55
|
-
`${notAnalyzed.length === 1 ? "" : "s"} could not be analyzed, so the scenario may ` +
|
|
56
|
-
`under-represent the real architecture. Fix the unanalyzed files and re-run.`,
|
|
57
|
-
);
|
|
50
|
+
// Check coverage — refused through the one structured contract
|
|
51
|
+
// `./coverage-verdict.mjs` builds (#608): the evaluation is withheld
|
|
52
|
+
// in-band, where a parser and `--output` can read it.
|
|
53
|
+
const completeness = coverageVerdict(commandContext);
|
|
54
|
+
if (!completeness.complete) {
|
|
55
|
+
return coverageRefusal({ command: "scenario", commandContext, what: "evaluating a scenario" });
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
// Evaluate
|
|
@@ -66,9 +64,7 @@ export function scenarioCommand(projectName, scenarioJson, commandContext, confi
|
|
|
66
64
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
67
65
|
imports: commandContext.analysis.imports.length,
|
|
68
66
|
notAnalyzed: [],
|
|
69
|
-
blindSpots:
|
|
70
|
-
.filter((f) => !isWholeFileFailure(f))
|
|
71
|
-
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
67
|
+
blindSpots: completeness.blindSpots,
|
|
72
68
|
notes: [
|
|
73
69
|
"scenario evaluation is virtual and not authoritative — run `check` for the real verdict",
|
|
74
70
|
"per-edge verdicts cover only depConstraints (3 of 15 violation types)",
|
|
@@ -103,7 +99,7 @@ export function scenarioCommand(projectName, scenarioJson, commandContext, confi
|
|
|
103
99
|
result,
|
|
104
100
|
});
|
|
105
101
|
|
|
106
|
-
const text = formatScenarioReport(scenario);
|
|
102
|
+
const text = formatScenarioReport(scenario, coverage);
|
|
107
103
|
|
|
108
104
|
return {
|
|
109
105
|
status: "ok",
|
|
@@ -119,15 +115,29 @@ export function scenarioCommand(projectName, scenarioJson, commandContext, confi
|
|
|
119
115
|
/**
|
|
120
116
|
* Formats a scenario evaluation as terminal text.
|
|
121
117
|
*
|
|
118
|
+
* The coverage line rides the same `coverageIncompleteReasons` clauses every
|
|
119
|
+
* other text face uses (`../verdict.mjs`), so a terminal reader is told how
|
|
120
|
+
* much of the tree the comparison inspected, in the one wording the JSON
|
|
121
|
+
* envelope's status also speaks (#609).
|
|
122
|
+
*
|
|
122
123
|
* @param {object} scenario The scenario evaluation result.
|
|
124
|
+
* @param {object} coverage The run's coverage block.
|
|
123
125
|
* @returns {string}
|
|
124
126
|
*/
|
|
125
|
-
function formatScenarioReport(scenario) {
|
|
127
|
+
function formatScenarioReport(scenario, coverage) {
|
|
126
128
|
const lines = [];
|
|
127
129
|
|
|
128
130
|
lines.push(`Scenario evaluation for "${scenario.project}"`);
|
|
129
131
|
lines.push(`${"=".repeat(50)}`);
|
|
130
132
|
lines.push(`Virtual: ${scenario.virtual} | Not authoritative: ${scenario.notAuthoritative}`);
|
|
133
|
+
// The "how much was inspected" line, derived from the same coverage block
|
|
134
|
+
// the envelope carries — never re-counted here.
|
|
135
|
+
lines.push(
|
|
136
|
+
`Coverage: ${coverage.imports} import${coverage.imports === 1 ? "" : "s"} in ` +
|
|
137
|
+
`${coverage.analyzedFiles} file${coverage.analyzedFiles === 1 ? "" : "s"} across ` +
|
|
138
|
+
`${coverage.projects} project${coverage.projects === 1 ? "" : "s"}` +
|
|
139
|
+
(coverage.complete ? "" : " — coverage incomplete"),
|
|
140
|
+
);
|
|
131
141
|
lines.push("");
|
|
132
142
|
|
|
133
143
|
if (scenario.changes.length > 0) {
|
package/src/commands/waivers.mjs
CHANGED
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
* `now` and the output is reproducible byte-for-byte. Defaults to the wall
|
|
38
38
|
* clock, the same injection `evaluate` uses for waiver expiry.
|
|
39
39
|
*/
|
|
40
|
-
import {
|
|
40
|
+
import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
41
41
|
import { suppressionCovers } from "../config.mjs";
|
|
42
42
|
import { referenceTime } from "../governance/clock.mjs";
|
|
43
43
|
import { isWaiver, remainingMs, waiverStatus } from "../governance/waiver.mjs";
|
|
44
|
+
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
44
45
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
45
46
|
import { formatWaiversReport } from "../report/waivers-text.mjs";
|
|
46
47
|
import { partitionUnownedCoverage } from "./coverage-acceptance.mjs";
|
|
@@ -150,10 +151,13 @@ export function computeWaivers(suppressions, rawViolations, now = referenceTime(
|
|
|
150
151
|
* clock, and — from `cli.mjs`'s `runWaivers` — the workspace-relative path
|
|
151
152
|
* the run's law actually resolved from, so the `coverage.unowned` matching
|
|
152
153
|
* below subtracts the same configuration files `check` subtracts.
|
|
153
|
-
* @returns {Promise<{status: "ok", waivers
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
154
|
+
* @returns {Promise<{status: "ok"|"no-verdict", waivers?: object, coverage: object,
|
|
155
|
+
* report: {text: string, json: string}}>}
|
|
156
|
+
* `status: "no-verdict"` carries no `waivers` payload — the verdict was
|
|
157
|
+
* withheld, and the envelope's `coverage` block is the whole answer (#608).
|
|
158
|
+
* @throws {Error} whenever the run's law is malformed — exit-3 class, the same
|
|
159
|
+
* posture `check` takes on a malformed config. Incomplete coverage returns
|
|
160
|
+
* the structured no-verdict envelope instead of throwing (#608).
|
|
157
161
|
*/
|
|
158
162
|
export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
|
|
159
163
|
const { root, provider, marker, analysis, graph } = commandContext;
|
|
@@ -180,25 +184,23 @@ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
|
|
|
180
184
|
// A waiver surface over a tree it could not fully read is a lottery ticket,
|
|
181
185
|
// not a surface: a file the analyzer never judged contributes no raw
|
|
182
186
|
// violation, so every waiver that names it reads as stale and the report
|
|
183
|
-
// says "covers nothing" about a finding the run never looked at.
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
`would read as covering nothing it never saw. Fix the unanalyzed files and re-run.`,
|
|
201
|
-
);
|
|
187
|
+
// says "covers nothing" about a finding the run never looked at. Refused
|
|
188
|
+
// through the one structured contract `./coverage-verdict.mjs` builds
|
|
189
|
+
// (#608) — "could not look" must never read as "looked and found nothing".
|
|
190
|
+
// A whole-file failure whose file a `coverage.unowned` row accepts is
|
|
191
|
+
// withdrawn first, exactly as `check` withdraws it (`./check.mjs`'s
|
|
192
|
+
// `acceptedUnclaimed`): its state is a recorded acceptance this very report
|
|
193
|
+
// is about to name, not a hole the run failed to look at.
|
|
194
|
+
const completeness = coverageVerdict(commandContext, {
|
|
195
|
+
acceptedFiles: unownedCoverage.acceptedFiles,
|
|
196
|
+
});
|
|
197
|
+
if (!completeness.complete) {
|
|
198
|
+
return coverageRefusal({
|
|
199
|
+
command: "waivers",
|
|
200
|
+
commandContext,
|
|
201
|
+
what: "measuring the waiver surface",
|
|
202
|
+
acceptedFiles: unownedCoverage.acceptedFiles,
|
|
203
|
+
});
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
// F07: a waiver surface measured against a graph that cannot see the
|
|
@@ -226,10 +228,13 @@ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
|
|
|
226
228
|
projects: Object.keys(graph.nodes).length,
|
|
227
229
|
analyzedFiles: analysis.analyzed,
|
|
228
230
|
imports: analysis.imports.length,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
// The withdrawn list — whole-file failures a `coverage.unowned` row
|
|
232
|
+
// accepts are already named in `result.unownedAcceptances` below, so
|
|
233
|
+
// repeating them here would double-count the same acceptance. On this
|
|
234
|
+
// path the list is empty by construction: anything unwithdrawn refused
|
|
235
|
+
// above.
|
|
236
|
+
notAnalyzed: completeness.notAnalyzed,
|
|
237
|
+
blindSpots: blindSpotRows(analysis.failures),
|
|
233
238
|
// `remainingMs` reflects the wall clock at the moment of THIS run, not the
|
|
234
239
|
// workspace — it is expected to differ between two runs of an unchanged
|
|
235
240
|
// tree, by design (`../governance/clock.mjs`). Disclosed here, in-band,
|
|
@@ -282,6 +287,9 @@ export async function waiversCommand(commandContext, boundaryConfig, io = {}) {
|
|
|
282
287
|
return {
|
|
283
288
|
status: "ok",
|
|
284
289
|
waivers: result,
|
|
290
|
+
// The same `coverage` block the envelope carries, so both return shapes —
|
|
291
|
+
// this one and the no-verdict refusal's (#608) — expose it under one key.
|
|
292
|
+
coverage,
|
|
285
293
|
report: {
|
|
286
294
|
text: formatWaiversReport(result),
|
|
287
295
|
json: renderJson(envelope),
|