@ecoma-io/archkeep 0.20.1 → 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 +708 -0
- 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 +691 -0
- 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-statement.mjs +31 -409
- package/src/commands/impact.mjs +18 -18
- package/src/commands/plan-context-command.mjs +10 -5
- package/src/commands/provenance-command.mjs +33 -2
- package/src/commands/reconcile.mjs +14 -17
- package/src/commands/scenario-evaluation.mjs +363 -198
- package/src/commands/scenario.mjs +32 -21
- package/src/commands/waivers.mjs +36 -28
- package/src/governance/evolution-event.mjs +62 -9
- package/src/governance/provenance-graph.mjs +479 -0
- package/src/intent/intent-manifest.json +83 -39
- package/src/report/json.mjs +32 -5
- package/src/report/provenance-text.mjs +30 -7
- 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
|
}
|