@ecoma-io/archkeep 0.21.0 → 0.22.0
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/package.json +1 -1
- package/src/analysis/contract.md +32 -5
- package/src/analysis/source-util.mjs +107 -0
- package/src/analysis/typescript.mjs +86 -5
- package/src/commands/change.mjs +59 -28
- 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 +184 -0
- package/src/commands/debt.mjs +18 -15
- package/src/commands/delta-classify.mjs +13 -18
- package/src/commands/delta.mjs +95 -33
- package/src/commands/diff.mjs +31 -24
- package/src/commands/discover.mjs +30 -10
- 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 +14 -5
- package/src/commands/health.mjs +12 -5
- package/src/commands/history.mjs +29 -15
- 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 +62 -9
- package/src/intent/intent-manifest.json +83 -39
- package/src/report/json.mjs +32 -5
- package/src/report/text.mjs +82 -12
- package/src/verdict.mjs +78 -36
- package/src/workspace.mjs +126 -2
package/src/commands/change.mjs
CHANGED
|
@@ -71,16 +71,20 @@
|
|
|
71
71
|
* change lives in `./change-intent.mjs`'s validation (`parseChangeIntent`),
|
|
72
72
|
* the same loud lane as every other malformed declaration.
|
|
73
73
|
*
|
|
74
|
-
* Refusals
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
74
|
+
* Refusals: incomplete head coverage returns the structured no-verdict
|
|
75
|
+
* envelope `./coverage-verdict.mjs` builds (#608) — status "no-verdict",
|
|
76
|
+
* exit 3, a `coverage` block naming every file and site the run could not
|
|
77
|
+
* judge — where a parser and `--output` can read it; the rest are throws →
|
|
78
|
+
* exit 3 upstream: a manifest that fails shape or reference validation, an
|
|
79
|
+
* unreadable/malformed/foreign-schema baseline, incomplete baseline coverage,
|
|
80
|
+
* a provider mismatch, an unregistered-plugin graph over polyglot manifests,
|
|
81
|
+
* and a run with no boundary law (constraints and the law fingerprint need
|
|
82
|
+
* one).
|
|
79
83
|
*
|
|
80
84
|
* This module computes and returns; `../../cli.mjs`'s `runChange` owns argv,
|
|
81
85
|
* output destination and the process exit code (`./README.md`).
|
|
82
86
|
*/
|
|
83
|
-
import { classifyDelta } from "./delta-classify.mjs";
|
|
87
|
+
import { classifyDelta, edgeEvolutionIdentity } from "./delta-classify.mjs";
|
|
84
88
|
import { computeDiff } from "./diff.mjs";
|
|
85
89
|
import { buildDependencies, buildProjects, computePolicyFingerprint } from "./graph.mjs";
|
|
86
90
|
import {
|
|
@@ -91,10 +95,12 @@ import {
|
|
|
91
95
|
} from "./change-intent.mjs";
|
|
92
96
|
import {
|
|
93
97
|
evidenceGraphToProjectGraph,
|
|
94
|
-
|
|
98
|
+
refusePluginGapHead,
|
|
95
99
|
sourceProjectAttributor,
|
|
96
100
|
} from "./delta.mjs";
|
|
97
101
|
import { providerMismatch, readEvidenceSnapshot } from "./delta-snapshot.mjs";
|
|
102
|
+
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
103
|
+
import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
98
104
|
import { cyclicProjects } from "../governance/fitness-rules.mjs";
|
|
99
105
|
import { fitnessVerdict } from "../governance/verdict.mjs";
|
|
100
106
|
import { buildDecision } from "../report/evidence.mjs";
|
|
@@ -154,25 +160,16 @@ function violationFindingId(entry) {
|
|
|
154
160
|
return `${entry.messageId}:${entry.sourceProject ?? "-"}:${entry.target}`;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
/**
|
|
158
|
-
* The identity string an observed edge carries into the event's `observed`
|
|
159
|
-
* and `affected` — `(source, target, type)`, the triple `./diff.mjs`'s
|
|
160
|
-
* `edgeIdentityKey` owns, spelled for a human reader (`>` separator, optional
|
|
161
|
-
* type suffix). Two spellings of one triple never diverge because the triple
|
|
162
|
-
* itself is the input.
|
|
163
|
-
*
|
|
164
|
-
* @param {{source: string, target: string, type?: string}} edge
|
|
165
|
-
* @returns {string}
|
|
166
|
-
*/
|
|
167
|
-
function edgeIdentityString(edge) {
|
|
168
|
-
return `${edge.source}>${edge.target}${edge.type === undefined || edge.type === "" ? "" : `:${edge.type}`}`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
163
|
/**
|
|
172
164
|
* The structural-diff facts in the event's `observed` shape (design §1),
|
|
173
165
|
* mapped from `computeDiff`'s output and the metadata comparison — the same
|
|
174
166
|
* lists the reconciliation already consumed, never recomputed.
|
|
175
167
|
*
|
|
168
|
+
* The edges' identity strings come from `edgeEvolutionIdentity`
|
|
169
|
+
* (`./delta-classify.mjs`) — the ONE spelling the evolution events'
|
|
170
|
+
* `observed.edges`/`affected.boundaries` use. A local spelling here would be
|
|
171
|
+
* a second definition of "same edge", and two definitions drift.
|
|
172
|
+
*
|
|
176
173
|
* @param {{addedProjects: object[], removedProjects: object[],
|
|
177
174
|
* changedProjects: object[], addedEdges: object[], removedEdges: object[]}} structural
|
|
178
175
|
* @param {{policyChanged: boolean|null, policyOneSided: boolean,
|
|
@@ -189,8 +186,8 @@ function observedFrom(structural, meta) {
|
|
|
189
186
|
changed: structural.changedProjects.map((project) => project.name),
|
|
190
187
|
};
|
|
191
188
|
const edges = {
|
|
192
|
-
added: structural.addedEdges.map(
|
|
193
|
-
removed: structural.removedEdges.map(
|
|
189
|
+
added: structural.addedEdges.map(edgeEvolutionIdentity),
|
|
190
|
+
removed: structural.removedEdges.map(edgeEvolutionIdentity),
|
|
194
191
|
};
|
|
195
192
|
return {
|
|
196
193
|
architectureChanged:
|
|
@@ -490,8 +487,14 @@ function judgeDeclaredConstraints(intent, io) {
|
|
|
490
487
|
* change event's `debt` diff judges the intent over this run's base and
|
|
491
488
|
* head graphs and would be untestable without it.
|
|
492
489
|
* @returns {Promise<{status: "ok"|"findings"|"no-verdict",
|
|
493
|
-
* changeIntent
|
|
494
|
-
*
|
|
490
|
+
* changeIntent?: object, coverage: object, report: {text: string, json: string}}>}
|
|
491
|
+
* `status: "no-verdict"` from the coverage refusal (#608) carries no
|
|
492
|
+
* `changeIntent` payload — the reconciliation was withheld, and the
|
|
493
|
+
* envelope's `coverage` block plus its `decision.reason` are the whole
|
|
494
|
+
* answer.
|
|
495
|
+
* @throws {Error} on every refusal the module header lists. Incomplete head
|
|
496
|
+
* coverage returns the structured no-verdict envelope instead of throwing
|
|
497
|
+
* (#608); the unregistered-plugin graph keeps its throw.
|
|
495
498
|
*/
|
|
496
499
|
export async function changeCommand(
|
|
497
500
|
baselinePath,
|
|
@@ -530,7 +533,23 @@ export async function changeCommand(
|
|
|
530
533
|
);
|
|
531
534
|
}
|
|
532
535
|
|
|
533
|
-
|
|
536
|
+
// The plugin-gap refusal stays a throw; the coverage refusal returns the one
|
|
537
|
+
// structured envelope `./coverage-verdict.mjs` builds (#608) — status
|
|
538
|
+
// "no-verdict", exit 3, a `coverage` block naming every file and site the
|
|
539
|
+
// run could not judge — instead of the throw `refuseUnjudgeableHead` used to
|
|
540
|
+
// carry here. Reconciling a declaration over a half-read tree would answer
|
|
541
|
+
// "undeclared" about architecture the run never observed, and that refusal
|
|
542
|
+
// belongs in-band, where a parser and `--output` can read it.
|
|
543
|
+
refusePluginGapHead(commandContext, "reconcile a change intent");
|
|
544
|
+
const completeness = coverageVerdict(commandContext);
|
|
545
|
+
if (!completeness.complete) {
|
|
546
|
+
return coverageRefusal({
|
|
547
|
+
command: "change",
|
|
548
|
+
commandContext,
|
|
549
|
+
what: "reconciling a change intent",
|
|
550
|
+
decision: true,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
534
553
|
|
|
535
554
|
const intent = await (readIntent ? readIntent(intentPath) : readChangeIntent(intentPath));
|
|
536
555
|
|
|
@@ -718,7 +737,12 @@ export async function changeCommand(
|
|
|
718
737
|
analyzedFiles: analysis.analyzed,
|
|
719
738
|
imports: analysis.imports.length,
|
|
720
739
|
notAnalyzed: [],
|
|
721
|
-
|
|
740
|
+
// The positioned failures the run SAW — including the dynamic and external
|
|
741
|
+
// sites that never withhold a verdict. This used to be hardcoded `[]`
|
|
742
|
+
// (#609): a declared limit named nowhere is a disclosure gap, and
|
|
743
|
+
// `blindSpotRows` is the one mapping every other command's coverage block
|
|
744
|
+
// carries.
|
|
745
|
+
blindSpots: blindSpotRows(analysis.failures),
|
|
722
746
|
notes,
|
|
723
747
|
};
|
|
724
748
|
|
|
@@ -745,7 +769,14 @@ export async function changeCommand(
|
|
|
745
769
|
reason: entry.reason,
|
|
746
770
|
}));
|
|
747
771
|
const evolution = classifyEvolution({
|
|
748
|
-
observed
|
|
772
|
+
// The raw triples, not the mapped strings `observed` carries (that object
|
|
773
|
+
// is the event's stored record): `classifyEvolution` owns the identity
|
|
774
|
+
// spelling and takes the triples, so `affected.boundaries` is mapped
|
|
775
|
+
// inside it under the one spelling rather than trusted from the caller.
|
|
776
|
+
observed: {
|
|
777
|
+
...observed,
|
|
778
|
+
edges: { added: structural.addedEdges, removed: structural.removedEdges },
|
|
779
|
+
},
|
|
749
780
|
...(classification === null
|
|
750
781
|
? {}
|
|
751
782
|
: {
|
package/src/commands/check.mjs
CHANGED
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
import { statSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
blindSpotRows,
|
|
17
|
+
fileFailure,
|
|
18
|
+
isWholeFileFailure,
|
|
19
|
+
unresolvableLiteralCount,
|
|
20
|
+
} from "../analysis/source-util.mjs";
|
|
16
21
|
import { tsconfigPathsFacts } from "../analysis/typescript.mjs";
|
|
17
22
|
import { stripTrailingSlashes } from "../path-util.mjs";
|
|
18
23
|
import { suppressionCovers } from "../config.mjs";
|
|
@@ -188,7 +193,7 @@ function declaredEdgeManifest({ provider, graph }, sourceProject) {
|
|
|
188
193
|
* intentUnresolved: number, intentUnresolvedDecisionRefs: number, fitnessFail: number,
|
|
189
194
|
* fitnessUnknown: number, customRuleFail: number, customRuleUnknown: number,
|
|
190
195
|
* customRuleEvidence: {rule: string, bytes: Uint8Array}[], customRulesDeclared: boolean,
|
|
191
|
-
* analyzed: number, unchecked: number, waived?: number}>}
|
|
196
|
+
* analyzed: number, unchecked: number, blindSpots: number, waived?: number}>}
|
|
192
197
|
*/
|
|
193
198
|
export async function check(options, { cwd, readGraph, listFiles = listTrackedFiles }) {
|
|
194
199
|
const commandContext = resolveCommandContext(
|
|
@@ -196,7 +201,7 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
196
201
|
{ readGraph, listFiles },
|
|
197
202
|
);
|
|
198
203
|
const { root, graph, workspace, tracked } = commandContext;
|
|
199
|
-
const { imports, exemptedFiles } = commandContext.analysis;
|
|
204
|
+
const { imports, exemptedFiles, unsupportedLanguageFiles } = commandContext.analysis;
|
|
200
205
|
const failures = [...commandContext.analysis.failures];
|
|
201
206
|
const analyzed = commandContext.analysis.analyzed;
|
|
202
207
|
|
|
@@ -622,6 +627,18 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
622
627
|
failures.filter(isWholeFileFailure).map((failure) => failure.sourceFile),
|
|
623
628
|
).size;
|
|
624
629
|
|
|
630
|
+
// Sites the run saw but never judged (#595): the file was analyzed, this
|
|
631
|
+
// site was not, and a pass over it would claim a verdict the run does not
|
|
632
|
+
// hold. Counted beside `unchecked` from one classifier
|
|
633
|
+
// (`unresolvableLiteralCount`) so the exit, the report and the envelope all
|
|
634
|
+
// agree from one number. The two classes that withholds nothing are
|
|
635
|
+
// excluded from the count: the declared dynamic limit — a non-literal
|
|
636
|
+
// `import()` argument, unknowable in principle — and the external
|
|
637
|
+
// bare-package site, whose resolvability depends on an installed dependency
|
|
638
|
+
// tree a workspace legitimately may not have (`isDynamicSiteFailure` and
|
|
639
|
+
// `isExternalSiteFailure` in source-util.mjs own the class line).
|
|
640
|
+
const blindSpotCount = unresolvableLiteralCount(failures);
|
|
641
|
+
|
|
625
642
|
// A row of the boundary law that covers nothing is a boundary that stopped
|
|
626
643
|
// being enforced, and — unlike a missing `reason`, which only a human can
|
|
627
644
|
// judge — it is machine-detectable. Two tables can be dead, and both are
|
|
@@ -838,8 +855,38 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
838
855
|
},
|
|
839
856
|
]
|
|
840
857
|
: []),
|
|
858
|
+
// Files a project owns that no analyzer claims (#601): skipped before
|
|
859
|
+
// reading, so they land in no failure list — without this row the run
|
|
860
|
+
// would present the judged surface as the whole story. Disclosure, not
|
|
861
|
+
// failure: the row names them and their extensions, and leaves
|
|
862
|
+
// `complete` to the surface that WAS judged. Sorted, because the row's
|
|
863
|
+
// bytes must not vary with `git ls-files`' order (E-F10).
|
|
864
|
+
...(unsupportedLanguageFiles.length > 0
|
|
865
|
+
? [{ kind: "unsupported-language", files: [...unsupportedLanguageFiles].sort() }]
|
|
866
|
+
: []),
|
|
841
867
|
];
|
|
842
868
|
|
|
869
|
+
// One verdict computation for both faces: the JSON envelope spreads it and
|
|
870
|
+
// the text report renders its `reasons` beside the headline — a second call
|
|
871
|
+
// here would let the two faces disagree about a run neither re-derives from
|
|
872
|
+
// the other (`../verdict.mjs`'s header owns that argument).
|
|
873
|
+
const verdict = verdictFor({
|
|
874
|
+
violations: violations.length,
|
|
875
|
+
declaredEdgeFindings,
|
|
876
|
+
goWorkDrift,
|
|
877
|
+
tsconfigPathsDead,
|
|
878
|
+
intentFindings,
|
|
879
|
+
intentUnresolved,
|
|
880
|
+
intentUnresolvedDecisionRefs: intentUnresolvedDecisionRefRows.length,
|
|
881
|
+
unchecked,
|
|
882
|
+
analyzed,
|
|
883
|
+
blindSpots: blindSpotCount,
|
|
884
|
+
fitnessFail,
|
|
885
|
+
fitnessUnknown,
|
|
886
|
+
customRuleFail,
|
|
887
|
+
customRuleUnknown,
|
|
888
|
+
});
|
|
889
|
+
|
|
843
890
|
const report =
|
|
844
891
|
options.format === "json"
|
|
845
892
|
? renderJson(
|
|
@@ -861,36 +908,20 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
861
908
|
// `decision` — the four-state verb of the same counts — so the
|
|
862
909
|
// envelope's `decision.verdict` and its `status` are built from
|
|
863
910
|
// exactly one computation and can never disagree.
|
|
864
|
-
...
|
|
865
|
-
violations: violations.length,
|
|
866
|
-
declaredEdgeFindings,
|
|
867
|
-
goWorkDrift,
|
|
868
|
-
tsconfigPathsDead,
|
|
869
|
-
intentFindings,
|
|
870
|
-
intentUnresolved,
|
|
871
|
-
intentUnresolvedDecisionRefs: intentUnresolvedDecisionRefRows.length,
|
|
872
|
-
unchecked,
|
|
873
|
-
fitnessFail,
|
|
874
|
-
fitnessUnknown,
|
|
875
|
-
customRuleFail,
|
|
876
|
-
customRuleUnknown,
|
|
877
|
-
}),
|
|
911
|
+
...verdict,
|
|
878
912
|
coverage: {
|
|
879
|
-
|
|
913
|
+
// Complete means the run judged everything in scope: no
|
|
914
|
+
// whole-file failure (unchecked), no unresolvable site (#595),
|
|
915
|
+
// and at least one file analyzed (#599 — a run that judged
|
|
916
|
+
// nothing has no verdict to claim).
|
|
917
|
+
complete: unchecked === 0 && blindSpotCount === 0 && analyzed > 0,
|
|
880
918
|
projects: Object.keys(graph.nodes).length,
|
|
881
919
|
analyzedFiles: analyzed,
|
|
882
920
|
imports: imports.length,
|
|
883
921
|
notAnalyzed: failures
|
|
884
922
|
.filter(isWholeFileFailure)
|
|
885
923
|
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason })),
|
|
886
|
-
blindSpots: failures
|
|
887
|
-
.filter((failure) => !isWholeFileFailure(failure))
|
|
888
|
-
.map(({ sourceFile, line, column, reason }) => ({
|
|
889
|
-
file: sourceFile,
|
|
890
|
-
line,
|
|
891
|
-
column,
|
|
892
|
-
reason,
|
|
893
|
-
})),
|
|
924
|
+
blindSpots: blindSpotRows(failures),
|
|
894
925
|
notes,
|
|
895
926
|
coverageGaps,
|
|
896
927
|
},
|
|
@@ -1020,6 +1051,10 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
1020
1051
|
// inspected" fact does (`../report/text.mjs`'s `formatReport`).
|
|
1021
1052
|
notes,
|
|
1022
1053
|
coverageGaps,
|
|
1054
|
+
// `formatReport` (text) renders these beside the headline; the SARIF
|
|
1055
|
+
// face files the same facts as warning notifications. Both faces of
|
|
1056
|
+
// one run name the same clauses, in the same order.
|
|
1057
|
+
coverageIncomplete: verdict.reasons,
|
|
1023
1058
|
// `formatReport` (text) reads this to annotate an unresolved
|
|
1024
1059
|
// decisionRef inline; `formatSarif` files each one as a warning
|
|
1025
1060
|
// notification. Both faces of one run name the same citations, in
|
|
@@ -1052,5 +1087,9 @@ export async function check(options, { cwd, readGraph, listFiles = listTrackedFi
|
|
|
1052
1087
|
customRulesDeclared: customRules !== null,
|
|
1053
1088
|
analyzed,
|
|
1054
1089
|
unchecked,
|
|
1090
|
+
// The site-level count `verdictFor` needs: `cli.mjs` passes this whole
|
|
1091
|
+
// return through `verdictFor` for the process's exit code, so a count the
|
|
1092
|
+
// envelope saw but the exit code did not would let the two disagree.
|
|
1093
|
+
blindSpots: blindSpotCount,
|
|
1055
1094
|
};
|
|
1056
1095
|
}
|
|
@@ -45,6 +45,72 @@ export const NOT_EVALUATED = EVALUATION_STATUS.NOT_EVALUATED;
|
|
|
45
45
|
export const UNSUPPORTED = EVALUATION_STATUS.UNSUPPORTED;
|
|
46
46
|
export const REFUSED = EVALUATION_STATUS.REFUSED;
|
|
47
47
|
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Evaluation contract types — which gates are required per evaluation type
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The evaluation contract types that determine which Evidence-Complete gates
|
|
54
|
+
* are required for `overallComplete`.
|
|
55
|
+
*
|
|
56
|
+
* - `canonical`: Standard architecture evaluation (no mutations, no scenario).
|
|
57
|
+
* Gates NOT required: mutationCoverage, surfaceParity, baseIdentityValid.
|
|
58
|
+
* - `scenario`: Hypothetical scenario evaluation. ALL gates required.
|
|
59
|
+
*
|
|
60
|
+
* @type {Readonly<{CANONICAL: string, SCENARIO: string}>}
|
|
61
|
+
*/
|
|
62
|
+
export const EVALUATION_CONTRACT_TYPES = Object.freeze({
|
|
63
|
+
CANONICAL: "canonical",
|
|
64
|
+
SCENARIO: "scenario",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Which Evidence-Complete gates are required for each contract type.
|
|
69
|
+
* A gate not listed here is still tracked and reported but does NOT block
|
|
70
|
+
* `overallComplete` — it is explicitly not applicable for that evaluation type.
|
|
71
|
+
*
|
|
72
|
+
* @type {Readonly<Object<string, ReadonlySet<string>>>}
|
|
73
|
+
*/
|
|
74
|
+
export const REQUIRED_GATES_FOR_CONTRACT = Object.freeze({
|
|
75
|
+
[EVALUATION_CONTRACT_TYPES.CANONICAL]: Object.freeze(
|
|
76
|
+
new Set([
|
|
77
|
+
"domainCoverage",
|
|
78
|
+
"claimEvidenceCoverage",
|
|
79
|
+
"causalCoverage",
|
|
80
|
+
"provenanceCoverage",
|
|
81
|
+
"hiddenGapCount",
|
|
82
|
+
"falseCompleteCount",
|
|
83
|
+
"deterministic",
|
|
84
|
+
]),
|
|
85
|
+
),
|
|
86
|
+
[EVALUATION_CONTRACT_TYPES.SCENARIO]: Object.freeze(
|
|
87
|
+
new Set([
|
|
88
|
+
"domainCoverage",
|
|
89
|
+
"claimEvidenceCoverage",
|
|
90
|
+
"causalCoverage",
|
|
91
|
+
"provenanceCoverage",
|
|
92
|
+
"mutationCoverage",
|
|
93
|
+
"surfaceParity",
|
|
94
|
+
"hiddenGapCount",
|
|
95
|
+
"falseCompleteCount",
|
|
96
|
+
"baseIdentityValid",
|
|
97
|
+
"deterministic",
|
|
98
|
+
]),
|
|
99
|
+
),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns true when the given gate key is required for the given contract type.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} gateKey The gate key (e.g. "domainCoverage").
|
|
106
|
+
* @param {string} [contractType] The evaluation contract type.
|
|
107
|
+
* Defaults to SCENARIO (most restrictive).
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
export function isGateRequired(gateKey, contractType = EVALUATION_CONTRACT_TYPES.SCENARIO) {
|
|
111
|
+
const required = REQUIRED_GATES_FOR_CONTRACT[contractType];
|
|
112
|
+
return required ? required.has(gateKey) : true;
|
|
113
|
+
}
|
|
48
114
|
// ---------------------------------------------------------------------------
|
|
49
115
|
// Evidence-Complete gate names — the canonical roster
|
|
50
116
|
// ---------------------------------------------------------------------------
|
|
@@ -69,6 +135,7 @@ export const EVIDENCE_COMPLETE_GATES = Object.freeze([
|
|
|
69
135
|
|
|
70
136
|
/**
|
|
71
137
|
* @typedef {object} EvidenceCompleteContract
|
|
138
|
+
* @property {string} contractType The evaluation contract type (canonical | scenario).
|
|
72
139
|
* @property {number} domainCoverage Ratio of evaluated required domains to required domains (0-1).
|
|
73
140
|
* @property {number} claimEvidenceCoverage Ratio of claims with valid evidence to material claims (0-1).
|
|
74
141
|
* @property {number} causalCoverage Ratio of consequences with complete causal chain to material consequences (0-1).
|
|
@@ -82,16 +149,16 @@ export const EVIDENCE_COMPLETE_GATES = Object.freeze([
|
|
|
82
149
|
* @property {string} overallStatus Overall Evidence-Complete status: "complete" | "incomplete" | "not_evaluated".
|
|
83
150
|
* @property {boolean} overallComplete True only when ALL required gates pass.
|
|
84
151
|
* @property {object} gates Individual gate statuses, keyed by gate name.
|
|
85
|
-
* @property {object} gates.domainCoverage Gate status.
|
|
86
|
-
* @property {object} gates.claimEvidenceCoverage Gate status.
|
|
87
|
-
* @property {object} gates.causalCoverage Gate status.
|
|
88
|
-
* @property {object} gates.provenanceCoverage Gate status.
|
|
89
|
-
* @property {object} gates.mutationCoverage Gate status.
|
|
90
|
-
* @property {object} gates.surfaceParity Gate status.
|
|
91
|
-
* @property {object} gates.hiddenGapCount Gate status.
|
|
92
|
-
* @property {object} gates.falseCompleteCount Gate status.
|
|
93
|
-
* @property {object} gates.baseIdentityValid Gate status.
|
|
94
|
-
* @property {object} gates.deterministic Gate status.
|
|
152
|
+
* @property {object} gates.domainCoverage Gate status with {value, pass, required}.
|
|
153
|
+
* @property {object} gates.claimEvidenceCoverage Gate status with {value, pass, required}.
|
|
154
|
+
* @property {object} gates.causalCoverage Gate status with {value, pass, required}.
|
|
155
|
+
* @property {object} gates.provenanceCoverage Gate status with {value, pass, required}.
|
|
156
|
+
* @property {object} gates.mutationCoverage Gate status with {value, pass, required}.
|
|
157
|
+
* @property {object} gates.surfaceParity Gate status with {value, pass, required}.
|
|
158
|
+
* @property {object} gates.hiddenGapCount Gate status with {value, pass, required}.
|
|
159
|
+
* @property {object} gates.falseCompleteCount Gate status with {value, pass, required}.
|
|
160
|
+
* @property {object} gates.baseIdentityValid Gate status with {value, pass, required}.
|
|
161
|
+
* @property {object} gates.deterministic Gate status with {value, pass, required}.
|
|
95
162
|
*/
|
|
96
163
|
|
|
97
164
|
// ---------------------------------------------------------------------------
|
|
@@ -306,8 +373,10 @@ export function buildCompleteness({
|
|
|
306
373
|
const statuses = Object.values(domains).map((d) => d.status);
|
|
307
374
|
const domainOverallComplete = statuses.every((s) => s === EVALUATION_STATUS.EVALUATED);
|
|
308
375
|
|
|
309
|
-
// If an Evidence-Complete contract is provided, enforce it as a gate
|
|
310
|
-
|
|
376
|
+
// If an Evidence-Complete contract is provided, enforce it as a gate.
|
|
377
|
+
// When no contract is provided, overallComplete MUST be false — the
|
|
378
|
+
// evaluation has not proven its evidence gates.
|
|
379
|
+
let ecComplete = false;
|
|
311
380
|
let falseCompleteCount = 0;
|
|
312
381
|
if (evidenceComplete) {
|
|
313
382
|
ecComplete = evidenceComplete.overallComplete;
|
|
@@ -346,6 +415,10 @@ export function buildCompleteness({
|
|
|
346
415
|
/**
|
|
347
416
|
* Builds an Evidence-Complete contract from the individual gate values.
|
|
348
417
|
*
|
|
418
|
+
* Only gates required for the given `contractType` are considered for
|
|
419
|
+
* `overallComplete`. Gates not required are still tracked and reported
|
|
420
|
+
* but do NOT block completeness.
|
|
421
|
+
*
|
|
349
422
|
* @param {object} gates
|
|
350
423
|
* @param {number} [gates.domainCoverage] Ratio (0-1).
|
|
351
424
|
* @param {number} [gates.claimEvidenceCoverage] Ratio (0-1).
|
|
@@ -357,6 +430,8 @@ export function buildCompleteness({
|
|
|
357
430
|
* @param {number} [gates.falseCompleteCount] Count (0 = pass).
|
|
358
431
|
* @param {boolean} [gates.baseIdentityValid] Boolean (true = pass).
|
|
359
432
|
* @param {boolean} [gates.deterministic] Boolean (true = pass).
|
|
433
|
+
* @param {string} [gates.contractType] Evaluation contract type for gate
|
|
434
|
+
* requirements (defaults to SCENARIO, the most restrictive).
|
|
360
435
|
* @returns {EvidenceCompleteContract}
|
|
361
436
|
*/
|
|
362
437
|
export function buildEvidenceComplete({
|
|
@@ -370,8 +445,9 @@ export function buildEvidenceComplete({
|
|
|
370
445
|
falseCompleteCount = -1,
|
|
371
446
|
baseIdentityValid = false,
|
|
372
447
|
deterministic = false,
|
|
448
|
+
contractType = EVALUATION_CONTRACT_TYPES.SCENARIO,
|
|
373
449
|
} = {}) {
|
|
374
|
-
const
|
|
450
|
+
const rawGates = {
|
|
375
451
|
domainCoverage: { value: domainCoverage, pass: domainCoverage === 1 },
|
|
376
452
|
claimEvidenceCoverage: { value: claimEvidenceCoverage, pass: claimEvidenceCoverage === 1 },
|
|
377
453
|
causalCoverage: { value: causalCoverage, pass: causalCoverage === 1 },
|
|
@@ -384,9 +460,21 @@ export function buildEvidenceComplete({
|
|
|
384
460
|
deterministic: { value: deterministic, pass: deterministic === true },
|
|
385
461
|
};
|
|
386
462
|
|
|
387
|
-
|
|
463
|
+
// Only required gates block overallComplete
|
|
464
|
+
const allRequiredPass = Object.keys(rawGates)
|
|
465
|
+
.filter((key) => isGateRequired(key, contractType))
|
|
466
|
+
.every((key) => rawGates[key].pass);
|
|
467
|
+
// Annotate each gate with whether it is required for this contract type
|
|
468
|
+
/** @type {any} */
|
|
469
|
+
const gates = Object.fromEntries(
|
|
470
|
+
Object.entries(rawGates).map(([key, gate]) => [
|
|
471
|
+
key,
|
|
472
|
+
{ ...gate, required: isGateRequired(key, contractType) },
|
|
473
|
+
]),
|
|
474
|
+
);
|
|
388
475
|
|
|
389
476
|
return {
|
|
477
|
+
contractType,
|
|
390
478
|
domainCoverage,
|
|
391
479
|
claimEvidenceCoverage,
|
|
392
480
|
causalCoverage,
|
|
@@ -397,8 +485,8 @@ export function buildEvidenceComplete({
|
|
|
397
485
|
falseCompleteCount,
|
|
398
486
|
baseIdentityValid,
|
|
399
487
|
deterministic,
|
|
400
|
-
overallStatus:
|
|
401
|
-
overallComplete:
|
|
488
|
+
overallStatus: allRequiredPass ? "complete" : "incomplete",
|
|
489
|
+
overallComplete: allRequiredPass,
|
|
402
490
|
gates,
|
|
403
491
|
};
|
|
404
492
|
}
|
|
@@ -414,16 +502,22 @@ export function buildEvidenceComplete({
|
|
|
414
502
|
export function assertEvidenceComplete(ec) {
|
|
415
503
|
if (ec.overallComplete) return;
|
|
416
504
|
|
|
505
|
+
const contractType = ec.contractType || EVALUATION_CONTRACT_TYPES.SCENARIO;
|
|
417
506
|
const failures = [];
|
|
418
507
|
for (const gate of EVIDENCE_COMPLETE_GATES) {
|
|
419
508
|
const g = ec.gates[gate.key];
|
|
509
|
+
// Skip non-required gates for this contract type
|
|
510
|
+
if (g.required === false) continue;
|
|
420
511
|
if (!g.pass) {
|
|
421
512
|
failures.push(`${gate.label}: ${JSON.stringify(g.value)} (expected pass)`);
|
|
422
513
|
}
|
|
423
514
|
}
|
|
424
515
|
|
|
516
|
+
if (failures.length === 0) return;
|
|
517
|
+
|
|
425
518
|
throw new Error(
|
|
426
519
|
`Evidence-Complete contract not satisfied.\n` +
|
|
520
|
+
` Contract type: ${contractType}\n` +
|
|
427
521
|
` Overall: ${ec.overallStatus}\n` +
|
|
428
522
|
` Failed gates:\n ${failures.join("\n ")}`,
|
|
429
523
|
);
|
|
@@ -571,15 +665,28 @@ export function buildScenarioCompleteness({
|
|
|
571
665
|
evidenceComplete,
|
|
572
666
|
});
|
|
573
667
|
|
|
574
|
-
// Recompute overall
|
|
668
|
+
// Recompute overall: base domains + EC gate + scenario domains.
|
|
669
|
+
// baseResult.overallComplete already includes the evidenceComplete gate,
|
|
670
|
+
// so reusing it prevents the silent-complete defect.
|
|
671
|
+
const overallComplete =
|
|
672
|
+
baseResult.overallComplete &&
|
|
673
|
+
changesDomain.status === EVALUATION_STATUS.EVALUATED &&
|
|
674
|
+
baseDomain.status === EVALUATION_STATUS.EVALUATED &&
|
|
675
|
+
mutationDomain.status === EVALUATION_STATUS.EVALUATED;
|
|
575
676
|
const allStatuses = [
|
|
576
677
|
...Object.values(baseResult.domains).map((d) => d.status),
|
|
577
678
|
changesDomain.status,
|
|
578
679
|
baseDomain.status,
|
|
579
680
|
mutationDomain.status,
|
|
580
681
|
];
|
|
581
|
-
|
|
582
|
-
|
|
682
|
+
// Include EC gate status in overall status: when evidenceComplete is
|
|
683
|
+
// provided and fails, overall status must reflect that.
|
|
684
|
+
const ecStatus = evidenceComplete
|
|
685
|
+
? evidenceComplete.overallComplete
|
|
686
|
+
? EVALUATION_STATUS.EVALUATED
|
|
687
|
+
: EVALUATION_STATUS.NOT_EVALUATED
|
|
688
|
+
: EVALUATION_STATUS.NOT_EVALUATED;
|
|
689
|
+
const overallStatus = worstStatus(...allStatuses, ecStatus);
|
|
583
690
|
|
|
584
691
|
return {
|
|
585
692
|
domains: baseResult.domains,
|
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
* than explaining constraints from a graph whose edges silently under-represent
|
|
33
33
|
* the real architecture.
|
|
34
34
|
*/
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
blindSpotRows,
|
|
37
|
+
isWholeFileFailure,
|
|
38
|
+
unresolvableLiteralCount,
|
|
39
|
+
} from "../analysis/source-util.mjs";
|
|
36
40
|
import { UsageError } from "../errors.mjs";
|
|
37
41
|
import { judgeEdge } from "./edge-constraints.mjs";
|
|
38
42
|
import { findConstraintsFor } from "../rules/tags.mjs";
|
|
@@ -150,7 +154,13 @@ export function contextCommand(projectName, commandContext, config) {
|
|
|
150
154
|
.filter(isWholeFileFailure)
|
|
151
155
|
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
// The same completeness `check` claims (#595, #599): unjudged sites and
|
|
158
|
+
// a zero-analyzed run defeat it here exactly as they do there, so a
|
|
159
|
+
// context report cannot look complete over a tree the run could not
|
|
160
|
+
// fully read.
|
|
161
|
+
const blindSpotCount = unresolvableLiteralCount(commandContext.analysis.failures);
|
|
162
|
+
const complete =
|
|
163
|
+
notAnalyzed.length === 0 && blindSpotCount === 0 && commandContext.analysis.analyzed > 0;
|
|
154
164
|
const status = complete ? "ok" : "no-verdict";
|
|
155
165
|
const exitCode = complete ? 0 : 3;
|
|
156
166
|
|
|
@@ -160,9 +170,7 @@ export function contextCommand(projectName, commandContext, config) {
|
|
|
160
170
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
161
171
|
imports: commandContext.analysis.imports.length,
|
|
162
172
|
notAnalyzed,
|
|
163
|
-
blindSpots: commandContext.analysis.failures
|
|
164
|
-
.filter((f) => !isWholeFileFailure(f))
|
|
165
|
-
.map(({ sourceFile, line, column, reason }) => ({ file: sourceFile, line, column, reason })),
|
|
173
|
+
blindSpots: blindSpotRows(commandContext.analysis.failures),
|
|
166
174
|
notes: [
|
|
167
175
|
"per-edge violations cover only depConstraints (3 of 15 violation types). " +
|
|
168
176
|
"A dependency with no violations here may still violate npm-ban, circular-dependency, " +
|
package/src/commands/context.mjs
CHANGED
|
@@ -214,11 +214,14 @@ export const WORKSPACE_MARKERS = [
|
|
|
214
214
|
* `createWorkspace` returns.
|
|
215
215
|
* @property {string[]} tracked Every tracked file, from `listFiles(root)`.
|
|
216
216
|
* @property {{imports: object[], failures: object[], analyzed: number,
|
|
217
|
-
* analyzedFiles: string[], exemptedFiles: string[]
|
|
217
|
+
* analyzedFiles: string[], exemptedFiles: string[],
|
|
218
|
+
* unsupportedLanguageFiles: string[]}} analysis The
|
|
218
219
|
* whole-tree-then-scoped (native) or scoped-then-analyzed (nx/moon) result —
|
|
219
220
|
* see the branches below for why the order is not the same on all three.
|
|
220
221
|
* `exemptedFiles` is always `[]` on nx/moon: `coverage.exempt` is a
|
|
221
|
-
* native-only `archkeep.json` key.
|
|
222
|
+
* native-only `archkeep.json` key. `unsupportedLanguageFiles` is scoped to
|
|
223
|
+
* the selected files on every branch (#601): a gap row names only files the
|
|
224
|
+
* selected scope could have judged.
|
|
222
225
|
* @property {{boundaryConfig: string|object, tsConfig: object|undefined,
|
|
223
226
|
* boundaryConfigDeclared: boolean, profiles?: string, inline?: boolean}} options
|
|
224
227
|
* What this workspace names its boundary law, its shared tsconfig, and —
|
|
@@ -549,6 +552,7 @@ export function resolveCommandContext(
|
|
|
549
552
|
let failures;
|
|
550
553
|
let analyzed;
|
|
551
554
|
let analyzedFiles;
|
|
555
|
+
let unsupportedLanguageFiles;
|
|
552
556
|
let pluginGap;
|
|
553
557
|
let unownedGap;
|
|
554
558
|
let unclaimedGap;
|
|
@@ -664,6 +668,12 @@ export function resolveCommandContext(
|
|
|
664
668
|
...jvmIndexFailures(workspace),
|
|
665
669
|
]);
|
|
666
670
|
analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
|
|
671
|
+
// Scoped exactly like `analyzedFiles` above (#601): a gap row must name
|
|
672
|
+
// only files the selected scope could have judged, and leaving this
|
|
673
|
+
// undefined here would crash `check`'s gap assembly instead of reporting.
|
|
674
|
+
unsupportedLanguageFiles = wholeTreeAnalysis.unsupportedLanguageFiles.filter((file) =>
|
|
675
|
+
selectedFiles.has(file),
|
|
676
|
+
);
|
|
667
677
|
analyzed = analyzedFiles.length;
|
|
668
678
|
// Unaffected by `paths`: an exempted file is by definition unowned by any
|
|
669
679
|
// project, so it was never a candidate for `owned`/`selected` in the
|
|
@@ -787,6 +797,13 @@ export function resolveCommandContext(
|
|
|
787
797
|
]);
|
|
788
798
|
unclaimedGap = { files: unclaimedFiles };
|
|
789
799
|
analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
|
|
800
|
+
// Scoped exactly like the native branch's own filter above (#601): a gap
|
|
801
|
+
// row must name only files the selected scope could have judged, and
|
|
802
|
+
// leaving this undefined here would crash `check`'s gap assembly instead
|
|
803
|
+
// of reporting.
|
|
804
|
+
unsupportedLanguageFiles = wholeTreeAnalysis.unsupportedLanguageFiles.filter((file) =>
|
|
805
|
+
selectedFiles.has(file),
|
|
806
|
+
);
|
|
790
807
|
analyzed = analyzedFiles.length;
|
|
791
808
|
// `coverage.exempt` is a native-only key (`../providers/native/coverage.mjs`'s
|
|
792
809
|
// header: "Nx has no equivalent question") — Moon carries no such list.
|
|
@@ -835,7 +852,10 @@ export function resolveCommandContext(
|
|
|
835
852
|
paths,
|
|
836
853
|
{ root, cwd, tracked },
|
|
837
854
|
);
|
|
838
|
-
({ imports, failures, analyzed, analyzedFiles } = analyzeWorkspace(
|
|
855
|
+
({ imports, failures, analyzed, analyzedFiles, unsupportedLanguageFiles } = analyzeWorkspace(
|
|
856
|
+
workspace,
|
|
857
|
+
selected,
|
|
858
|
+
));
|
|
839
859
|
// Unclaimed analyzable files — `unclaimedFileFailures` above — join
|
|
840
860
|
// unconditionally, the same workspace-wide posture native's own
|
|
841
861
|
// `discovered.failures` has, so a scoped `check <path>` cannot hide an
|
|
@@ -876,7 +896,14 @@ export function resolveCommandContext(
|
|
|
876
896
|
graph,
|
|
877
897
|
workspace,
|
|
878
898
|
tracked,
|
|
879
|
-
analysis: {
|
|
899
|
+
analysis: {
|
|
900
|
+
imports,
|
|
901
|
+
failures,
|
|
902
|
+
analyzed,
|
|
903
|
+
analyzedFiles,
|
|
904
|
+
exemptedFiles,
|
|
905
|
+
unsupportedLanguageFiles,
|
|
906
|
+
},
|
|
880
907
|
options,
|
|
881
908
|
pluginGap,
|
|
882
909
|
unownedGap,
|