@ecoma-io/archkeep 0.23.0 → 0.24.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/package.json +1 -1
- package/src/analysis/csharp.mjs +3 -1
- package/src/analysis/dotnet/csproj.mjs +5 -1
- package/src/analysis/dotnet/namespaces.mjs +1 -0
- package/src/analysis/go.mjs +6 -0
- package/src/analysis/java.mjs +2 -0
- package/src/analysis/jvm/gradle.mjs +3 -1
- package/src/analysis/jvm/maven.mjs +6 -1
- package/src/analysis/jvm/packages.mjs +1 -0
- package/src/analysis/jvm/resolve.mjs +4 -2
- package/src/analysis/kotlin.mjs +2 -0
- package/src/analysis/markdown.mjs +341 -0
- package/src/analysis/python.mjs +8 -0
- package/src/analysis/rust.mjs +5 -1
- package/src/analysis/source-util.mjs +6 -5
- package/src/analysis/typescript.mjs +148 -0
- package/src/architecture-intent/model.mjs +13 -8
- package/src/architecture-intent/selectors.mjs +2 -1
- package/src/commands/change-intent.mjs +10 -9
- package/src/commands/change.mjs +17 -1
- package/src/commands/check.mjs +177 -17
- package/src/commands/completeness.mjs +7 -6
- package/src/commands/context-command.mjs +12 -20
- package/src/commands/coverage-acceptance.mjs +46 -0
- package/src/commands/coverage-verdict.mjs +12 -2
- package/src/commands/custom-rules.mjs +1 -0
- package/src/commands/delta-classify.mjs +3 -0
- package/src/commands/delta-snapshot.mjs +3 -6
- package/src/commands/delta.mjs +35 -21
- package/src/commands/drift.mjs +1 -1
- package/src/commands/evaluation-primitives.mjs +4 -4
- package/src/commands/evolution.mjs +2 -0
- package/src/commands/explain.mjs +19 -20
- package/src/commands/graph.mjs +44 -15
- package/src/commands/health.mjs +4 -0
- package/src/commands/history.mjs +2 -0
- package/src/commands/plan-context-command.mjs +9 -2
- package/src/commands/policy.mjs +8 -5
- package/src/commands/provenance.mjs +8 -2
- package/src/commands/scenario-evaluation.mjs +1 -1
- package/src/commands/trajectory.mjs +2 -1
- package/src/config.mjs +170 -2
- package/src/custom-rules/host.mjs +3 -3
- package/src/errors.mjs +23 -1
- package/src/eslint-config.mjs +3 -5
- package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
- package/src/go-work.mjs +1 -1
- package/src/governance/adr-registry.mjs +6 -2
- package/src/governance/debt-ledger.mjs +1 -1
- package/src/governance/decision-fitness.mjs +2 -0
- package/src/governance/decision-graph.mjs +1 -0
- package/src/governance/discovery-proposal.mjs +8 -2
- package/src/governance/evolution-event.mjs +42 -0
- package/src/governance/evolution-store.mjs +3 -2
- package/src/governance/fitness-registry.mjs +2 -3
- package/src/governance/preset-fingerprints.json +14 -14
- package/src/governance/profile-registry.mjs +22 -3
- package/src/governance/provenance-record.mjs +4 -1
- package/src/governance/reconcile-score.mjs +4 -0
- package/src/governance/row-schema.mjs +1 -3
- package/src/governance/verdict.mjs +1 -0
- package/src/governance/waiver.mjs +1 -0
- package/src/intent/intent-manifest.json +6 -6
- package/src/intent/mask-non-code.mjs +1 -0
- package/src/lsp/diagnostics.mjs +3 -2
- package/src/lsp/protocol.mjs +2 -1
- package/src/lsp/server.mjs +3 -0
- package/src/lsp/workspace-index.mjs +3 -1
- package/src/providers/moon.mjs +5 -5
- package/src/providers/native/differential.fixtures.mjs +29 -11
- package/src/providers/native/index.mjs +2 -1
- package/src/providers/native/model.mjs +4 -0
- package/src/report/envelope-shape.mjs +2 -0
- package/src/report/sarif.mjs +21 -8
- package/src/report/snapshot-text.mjs +3 -3
- package/src/report/text.mjs +10 -2
- package/src/rules/index.mjs +30 -0
- package/src/rules/match.mjs +7 -5
- package/src/rules/specifiers.mjs +2 -0
- package/src/rules/tags.mjs +3 -2
- package/src/rules/topology.mjs +6 -1
- package/src/verdict.mjs +33 -2
- package/src/workspace.mjs +1 -0
package/src/report/text.mjs
CHANGED
|
@@ -94,6 +94,7 @@ function formatEdge(violation) {
|
|
|
94
94
|
* @returns {string}
|
|
95
95
|
*/
|
|
96
96
|
export function formatViolation(violation, unresolvedDecisionRefs) {
|
|
97
|
+
// used by its own test
|
|
97
98
|
const message = violation.message
|
|
98
99
|
.split("\n")
|
|
99
100
|
.map((line) => (line === "" ? "" : `${CONTINUED}${line}`))
|
|
@@ -159,6 +160,7 @@ const formatFailure = (failure) =>
|
|
|
159
160
|
* @returns {string}
|
|
160
161
|
*/
|
|
161
162
|
export function formatFailures(failures) {
|
|
163
|
+
// used by its own test
|
|
162
164
|
if (failures.length === 0) return "";
|
|
163
165
|
const unchecked = failures.filter(isWholeFileFailure);
|
|
164
166
|
const blind = failures.filter((failure) => !isWholeFileFailure(failure));
|
|
@@ -236,6 +238,7 @@ export function formatFailures(failures) {
|
|
|
236
238
|
* @returns {string} Empty exactly when there is no go.work verdict to render.
|
|
237
239
|
*/
|
|
238
240
|
export function formatGoWork(goWork) {
|
|
241
|
+
// used by its own test
|
|
239
242
|
if (goWork == null) return "";
|
|
240
243
|
const { findings, moduleProjects } = goWork;
|
|
241
244
|
const modules = `${moduleProjects} Go module project${moduleProjects === 1 ? "" : "s"}`;
|
|
@@ -291,6 +294,7 @@ export function formatGoWork(goWork) {
|
|
|
291
294
|
* @returns {string} Empty exactly when there is no declared-edge verdict to render.
|
|
292
295
|
*/
|
|
293
296
|
export function formatDeclaredEdges(declaredEdges, unresolvedDecisionRefs) {
|
|
297
|
+
// used by its own test
|
|
294
298
|
if (declaredEdges == null) return "";
|
|
295
299
|
const { findings, judged, declaration = "implicitDependencies" } = declaredEdges;
|
|
296
300
|
const label = `${judged} implicit edge${judged === 1 ? "" : "s"} judged`;
|
|
@@ -345,6 +349,7 @@ export function formatDeclaredEdges(declaredEdges, unresolvedDecisionRefs) {
|
|
|
345
349
|
* @returns {string} Empty exactly when there is no paths verdict to render.
|
|
346
350
|
*/
|
|
347
351
|
export function formatTsconfigPaths(tsconfigPaths) {
|
|
352
|
+
// used by its own test
|
|
348
353
|
if (tsconfigPaths == null) return "";
|
|
349
354
|
const { tsConfig, findings, aliases, unjudged } = tsconfigPaths;
|
|
350
355
|
const judged =
|
|
@@ -393,7 +398,7 @@ export function formatTsconfigPaths(tsconfigPaths) {
|
|
|
393
398
|
* unresolvedDecisionRefs?: {kind: string, decisionRef: string}[]}|null|undefined} intent
|
|
394
399
|
* @returns {string} Empty exactly when there is no intent verdict to render.
|
|
395
400
|
*/
|
|
396
|
-
|
|
401
|
+
function formatIntentSection(intent) {
|
|
397
402
|
if (intent == null) return "";
|
|
398
403
|
const { verdict, findings, unresolved, boundaries, unresolvedDecisionRefs = [] } = intent;
|
|
399
404
|
const count = boundaries.length;
|
|
@@ -542,6 +547,7 @@ function customFindingSite(finding) {
|
|
|
542
547
|
* @returns {string} Empty exactly when the policy declared no custom rules.
|
|
543
548
|
*/
|
|
544
549
|
export function formatCustomRulesSection(customRules) {
|
|
550
|
+
// used by its own test
|
|
545
551
|
if (customRules == null || customRules.decisions.length === 0) return "";
|
|
546
552
|
const { decisions, overall } = customRules;
|
|
547
553
|
const entries = decisions.map((decision) => {
|
|
@@ -596,6 +602,7 @@ export function formatCustomRulesSection(customRules) {
|
|
|
596
602
|
* @returns {string} Empty exactly when there is no coverage gap to render.
|
|
597
603
|
*/
|
|
598
604
|
export function formatCoverageGaps(coverageGaps) {
|
|
605
|
+
// used by its own test
|
|
599
606
|
if (coverageGaps.length === 0) return "";
|
|
600
607
|
return coverageGaps.map(formatCoverageGap).join("\n");
|
|
601
608
|
}
|
|
@@ -792,6 +799,7 @@ function formatUntrackedFilesGap(gap) {
|
|
|
792
799
|
* @returns {string} Empty exactly when no policy identity was supplied.
|
|
793
800
|
*/
|
|
794
801
|
export function formatPolicy(policy) {
|
|
802
|
+
// used by its own test
|
|
795
803
|
if (policy == null) return "";
|
|
796
804
|
const { profile, source, fingerprint } = policy;
|
|
797
805
|
const law = profile === null ? source : `profile "${profile}" from ${source}`;
|
|
@@ -812,7 +820,7 @@ export function formatPolicy(policy) {
|
|
|
812
820
|
* @param {Set<string>} [unresolvedDecisionRefs] Forwarded to `formatViolation`.
|
|
813
821
|
* @returns {string}
|
|
814
822
|
*/
|
|
815
|
-
|
|
823
|
+
function formatAcceptedViolations(waived, unresolvedDecisionRefs) {
|
|
816
824
|
const rows = waived.map((violation) => {
|
|
817
825
|
const waiver = violation.waivedBy;
|
|
818
826
|
return [
|
package/src/rules/index.mjs
CHANGED
|
@@ -815,6 +815,36 @@ function annotatedByTable(suppressions, violation, now) {
|
|
|
815
815
|
return violation;
|
|
816
816
|
}
|
|
817
817
|
|
|
818
|
+
/**
|
|
819
|
+
* The suppression table applied to verdicts that did not come from
|
|
820
|
+
* `evaluateRun`'s site walk — suppressing rows remove, active waivers mark
|
|
821
|
+
* `waivedBy`, expired ones re-assert with `evidence`, exactly as the site walk
|
|
822
|
+
* applies them, because a verdict must ride the same table whichever walk
|
|
823
|
+
* produced it.
|
|
824
|
+
*
|
|
825
|
+
* The one caller is `./commands/check.mjs`'s markdown document track: its
|
|
826
|
+
* edges are judged per edge through `./edge-constraints.mjs`'s `judgeEdge` —
|
|
827
|
+
* they have no import site for `candidateGroupsFor` to walk, so there is no
|
|
828
|
+
* group chain, only a flat list — but a suppression row that names a document
|
|
829
|
+
* must silence the same verdict here it would silence on an import site, and
|
|
830
|
+
* a waiver over one must annotate it the same way. Unlike the site walk there
|
|
831
|
+
* is no second candidate group to fall through to: an edge's verdicts are all
|
|
832
|
+
* reported together, so a row that removes some leaves the rest standing
|
|
833
|
+
* rather than promoting anything.
|
|
834
|
+
*
|
|
835
|
+
* @param {object[]} suppressions The validated `boundarySuppressions` table.
|
|
836
|
+
* @param {object[]} violations The unfiltered verdicts, in walk order.
|
|
837
|
+
* @param {string} [now] Reference instant for waiver expiry; defaults to the
|
|
838
|
+
* shared governance clock, as `evaluateRun` does.
|
|
839
|
+
* @returns {object[]} The survivors, in input order, annotations applied.
|
|
840
|
+
*/
|
|
841
|
+
export function applySuppressionTable(suppressions, violations, now = referenceTime()) {
|
|
842
|
+
if (suppressions.length === 0) return violations;
|
|
843
|
+
return violations
|
|
844
|
+
.filter((violation) => !removedByTable(suppressions, violation, now))
|
|
845
|
+
.map((violation) => annotatedByTable(suppressions, violation, now));
|
|
846
|
+
}
|
|
847
|
+
|
|
818
848
|
/**
|
|
819
849
|
* One run of the engine over every import site: the judged verdict per site
|
|
820
850
|
* plus the raw superset it was picked from.
|
package/src/rules/match.mjs
CHANGED
|
@@ -85,7 +85,7 @@ import { posix } from "node:path";
|
|
|
85
85
|
* costs is bounded at the other end, by `MAX_SPECIFIER_LENGTH` below, because
|
|
86
86
|
* a degree-2 pattern is still quadratic in a subject nothing else limits.
|
|
87
87
|
*/
|
|
88
|
-
|
|
88
|
+
const MAX_RESPLIT_REPETITIONS = 2;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* The most repetitions a SINGLE-DELIMITER chain may carry — the one shape
|
|
@@ -114,7 +114,7 @@ export const MAX_RESPLIT_REPETITIONS = 2;
|
|
|
114
114
|
* path pattern naming every segment of `libs/<area>/<name>/<entry>` needs
|
|
115
115
|
* four.
|
|
116
116
|
*/
|
|
117
|
-
export const MAX_DELIMITED_SEGMENTS = 8;
|
|
117
|
+
export const MAX_DELIMITED_SEGMENTS = 8; // used by its own test
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* The longest import specifier this engine will match a consumer-written
|
|
@@ -148,7 +148,7 @@ export const MAX_DELIMITED_SEGMENTS = 8;
|
|
|
148
148
|
* 1024 `b`s. That is the number to compare a change here against; before this
|
|
149
149
|
* bound and the refusals above, the same sweep found 59 SECONDS.
|
|
150
150
|
*/
|
|
151
|
-
export const MAX_SPECIFIER_LENGTH = 1024;
|
|
151
|
+
export const MAX_SPECIFIER_LENGTH = 1024; // used by its own test
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
154
|
* The deepest group nesting this model reads. Past it a pattern is refused
|
|
@@ -1123,6 +1123,7 @@ function tradesWithNeighbour(elements, index, last) {
|
|
|
1123
1123
|
* @returns {string|null}
|
|
1124
1124
|
*/
|
|
1125
1125
|
export function regexComplexityError(source) {
|
|
1126
|
+
// used by its own test
|
|
1126
1127
|
const { root, tooDeep } = parsePattern(source);
|
|
1127
1128
|
if (tooDeep) {
|
|
1128
1129
|
return (
|
|
@@ -1174,7 +1175,7 @@ function resplitReason(degree) {
|
|
|
1174
1175
|
* @param {string} specifier
|
|
1175
1176
|
* @returns {string|null}
|
|
1176
1177
|
*/
|
|
1177
|
-
|
|
1178
|
+
function specifierLengthError(specifier) {
|
|
1178
1179
|
if (specifier.length <= MAX_SPECIFIER_LENGTH) return null;
|
|
1179
1180
|
return (
|
|
1180
1181
|
`is ${specifier.length} characters, past the ${MAX_SPECIFIER_LENGTH} this engine will match ` +
|
|
@@ -1538,7 +1539,7 @@ export function findMatchingProjects(patterns, nodes) {
|
|
|
1538
1539
|
* anything a real suppression, exemption or project rule plausibly needs to
|
|
1539
1540
|
* name from ONE glob string.
|
|
1540
1541
|
*/
|
|
1541
|
-
export const MAX_GLOB_EXPANSIONS = 512;
|
|
1542
|
+
export const MAX_GLOB_EXPANSIONS = 512; // used by its own test
|
|
1542
1543
|
|
|
1543
1544
|
/**
|
|
1544
1545
|
* A brace group's content matches one of these two shapes instead of a
|
|
@@ -1620,6 +1621,7 @@ function rangeCardinality(content) {
|
|
|
1620
1621
|
* certain to exceed `cap`.
|
|
1621
1622
|
*/
|
|
1622
1623
|
export function braceExpansionCount(pattern, cap) {
|
|
1624
|
+
// used by its own test
|
|
1623
1625
|
const limit = cap + 1;
|
|
1624
1626
|
/** @type {{alternatives: number, branchProduct: number, start: number}[]} */
|
|
1625
1627
|
const frames = [{ alternatives: 0, branchProduct: 1, start: 0 }];
|
package/src/rules/specifiers.mjs
CHANGED
|
@@ -48,6 +48,7 @@ export const DEFAULT_WORKSPACE_LAYOUT = Object.freeze({ libsDir: "libs", appsDir
|
|
|
48
48
|
|
|
49
49
|
/** `./x` or `../x` — upstream's `isRelative`, from `runtime-lint-utils`. */
|
|
50
50
|
export function isRelative(s) {
|
|
51
|
+
// used by its own test
|
|
51
52
|
return s.startsWith("./") || s.startsWith("../");
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -191,6 +192,7 @@ export function getTargetProjectBasedOnRelativeImport(imp, sourceFile, projectRo
|
|
|
191
192
|
* @returns {boolean}
|
|
192
193
|
*/
|
|
193
194
|
export function isConstraintBanningProject(externalProject, constraint, imp) {
|
|
195
|
+
// used by its own test
|
|
194
196
|
assertMatchableSpecifier(imp, "import specifier judged against the constraint table");
|
|
195
197
|
const { allowedExternalImports, bannedExternalImports } = constraint;
|
|
196
198
|
const { packageName } = externalProject.data;
|
package/src/rules/tags.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { tagMatches } from "./match.mjs";
|
|
|
29
29
|
import { getPath, pathExists } from "./reachability.mjs";
|
|
30
30
|
|
|
31
31
|
/** `"a", "b"` — how upstream renders a tag list inside a message. */
|
|
32
|
-
|
|
32
|
+
function stringifyTags(tags) {
|
|
33
33
|
return tags.map((t) => `"${t}"`).join(", ");
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -55,7 +55,7 @@ export function hasTag(project, tag) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** True when the project carries NONE of these tags — upstream's spelling. */
|
|
58
|
-
|
|
58
|
+
function hasNoneOfTheseTags(project, tags) {
|
|
59
59
|
return tags.filter((tag) => hasTag(project, tag)).length === 0;
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -165,6 +165,7 @@ export function orphanedNotDependOnTags(depConstraints, graph) {
|
|
|
165
165
|
* are what the message prints so a reader can see the hop that did it.
|
|
166
166
|
*/
|
|
167
167
|
export function findDependenciesWithTags(targetProject, tags, graph, reach) {
|
|
168
|
+
// used by its own test
|
|
168
169
|
const reachable = Object.keys(graph.nodes)
|
|
169
170
|
.sort()
|
|
170
171
|
.filter(
|
package/src/rules/topology.mjs
CHANGED
|
@@ -57,6 +57,7 @@ export function appIsMFERemote(project) {
|
|
|
57
57
|
* — the common case, and the one that keeps `noSelfCircularDependencies` armed.
|
|
58
58
|
*/
|
|
59
59
|
export function entryPointOf(file, projectRoot, entryPoints) {
|
|
60
|
+
// used by its own test
|
|
60
61
|
if (!entryPoints || entryPoints.length === 0) return undefined;
|
|
61
62
|
const fileEntryPoint = entryPoints.find((entry) => entry.file === file);
|
|
62
63
|
if (fileEntryPoint) return fileEntryPoint.file;
|
|
@@ -88,7 +89,7 @@ export function entryPointOf(file, projectRoot, entryPoints) {
|
|
|
88
89
|
* standing in for upstream's module resolution — the resolver already ran; this
|
|
89
90
|
* layer never runs a second one.
|
|
90
91
|
*/
|
|
91
|
-
|
|
92
|
+
function secondaryEntryPointPath(resolvedFile, targetProject) {
|
|
92
93
|
if (!resolvedFile) return undefined;
|
|
93
94
|
return entryPointOf(resolvedFile, targetProject.data.root, targetProject.data?.entryPoints);
|
|
94
95
|
}
|
|
@@ -128,6 +129,7 @@ export function belongsToDifferentEntryPoint(resolvedFile, sourceFile, sourcePro
|
|
|
128
129
|
* @returns {string[]|null}
|
|
129
130
|
*/
|
|
130
131
|
export function findDynamicImportPath(graph, sourceProjectName, targetProjectName, visited = []) {
|
|
132
|
+
// used by its own test
|
|
131
133
|
if (visited.indexOf(sourceProjectName) > -1) return null;
|
|
132
134
|
for (const dependency of graph.dependencies?.[sourceProjectName] ?? []) {
|
|
133
135
|
if (dependency.type !== DYNAMIC) continue;
|
|
@@ -152,6 +154,7 @@ export function findDynamicImportPath(graph, sourceProjectName, targetProjectNam
|
|
|
152
154
|
* fired, and then the verdict would name evidence for a route nobody walked.
|
|
153
155
|
*/
|
|
154
156
|
export function hasDynamicImport(graph, sourceProjectName, targetProjectName) {
|
|
157
|
+
// used by its own test
|
|
155
158
|
return findDynamicImportPath(graph, sourceProjectName, targetProjectName) !== null;
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -209,6 +212,7 @@ export function createFileDependencyIndex(edges) {
|
|
|
209
212
|
|
|
210
213
|
/** The files carrying each hop of a path — one entry per consecutive pair. */
|
|
211
214
|
export function findFilesInCircularPath(fileIndex, circularPath) {
|
|
215
|
+
// used by its own test
|
|
212
216
|
const chain = [];
|
|
213
217
|
for (let i = 0; i < circularPath.length - 1; i++) {
|
|
214
218
|
chain.push(fileIndex.any.get(edgeKey(circularPath[i].name, circularPath[i + 1].name)) ?? []);
|
|
@@ -218,6 +222,7 @@ export function findFilesInCircularPath(fileIndex, circularPath) {
|
|
|
218
222
|
|
|
219
223
|
/** The files that lazy-load `target` from inside `source`. */
|
|
220
224
|
export function findFilesWithDynamicImports(fileIndex, sourceProjectName, targetProjectName) {
|
|
225
|
+
// used by its own test
|
|
221
226
|
return fileIndex.dynamic.get(edgeKey(sourceProjectName, targetProjectName)) ?? [];
|
|
222
227
|
}
|
|
223
228
|
|
package/src/verdict.mjs
CHANGED
|
@@ -61,6 +61,33 @@ export function coverageIncompleteReasons({ unchecked, blindSpots, analyzed }) {
|
|
|
61
61
|
analyzed === 0 ? "no file in scope could be analyzed — coverage incomplete" : null,
|
|
62
62
|
].filter(Boolean);
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The one completeness predicate — the three coverage axes conjoined, the
|
|
67
|
+
* boolean twin of `coverageIncompleteReasons` directly above, which words the
|
|
68
|
+
* same axes as clauses. `coverageVerdict` (`./commands/coverage-verdict.mjs`)
|
|
69
|
+
* reads its `complete` from here, `verdictFor` reads the decision's
|
|
70
|
+
* `coverageComplete` from here, and `check`'s coverage block reads its
|
|
71
|
+
* `complete` from here — three faces of one claim, so the envelope's
|
|
72
|
+
* `coverage.complete` and its `decision.coverageComplete` cannot disagree
|
|
73
|
+
* about a run neither re-derives from the other.
|
|
74
|
+
*
|
|
75
|
+
* The counts come from the caller because a command's coverage universe is
|
|
76
|
+
* its own: `check`'s is wider than `commandContext.analysis` (the go.work and
|
|
77
|
+
* tsconfig whole-file failures it pushes, the accepted `coverage.unowned`
|
|
78
|
+
* files it withdraws), and each face feeds the counts it is a claim about. A
|
|
79
|
+
* caller that reads plain `commandContext.analysis` should call
|
|
80
|
+
* `coverageVerdict` instead — this predicate is the law's last step, not the
|
|
81
|
+
* place failure classes get decided (`./analysis/source-util.mjs`'s
|
|
82
|
+
* classifiers own that line).
|
|
83
|
+
*
|
|
84
|
+
* @param {{unchecked: number, blindSpotCount: number, analyzed: number}} counts
|
|
85
|
+
* @returns {boolean} Whether the run judged everything in its scope.
|
|
86
|
+
*/
|
|
87
|
+
export function coverageComplete({ unchecked, blindSpotCount, analyzed }) {
|
|
88
|
+
return unchecked === 0 && blindSpotCount === 0 && analyzed > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
64
91
|
/**
|
|
65
92
|
* The one place that turns a run's counts into the verdict every format
|
|
66
93
|
* agrees on. `runCheck` uses it for the process's exit code; `check` uses the
|
|
@@ -127,7 +154,11 @@ export function verdictFor({
|
|
|
127
154
|
reasons: coverageReasons,
|
|
128
155
|
decision: buildDecision({
|
|
129
156
|
status: "findings",
|
|
130
|
-
|
|
157
|
+
// The one completeness predicate, not a restatement: the decision's
|
|
158
|
+
// `coverageComplete` and the envelope's `coverage.complete` are the
|
|
159
|
+
// same claim about the same counts (`check` feeds both from one
|
|
160
|
+
// object), so they read it from one expression.
|
|
161
|
+
coverageComplete: coverageComplete({ unchecked, blindSpotCount: blindSpots, analyzed }),
|
|
131
162
|
findings:
|
|
132
163
|
violations +
|
|
133
164
|
declaredEdgeFindings +
|
|
@@ -188,7 +219,7 @@ export function verdictFor({
|
|
|
188
219
|
reasons,
|
|
189
220
|
decision: buildDecision({
|
|
190
221
|
status: "no-verdict",
|
|
191
|
-
coverageComplete: unchecked
|
|
222
|
+
coverageComplete: coverageComplete({ unchecked, blindSpotCount: blindSpots, analyzed }),
|
|
192
223
|
findings: 0,
|
|
193
224
|
reason: reasons.join("; "),
|
|
194
225
|
}),
|
package/src/workspace.mjs
CHANGED
|
@@ -721,6 +721,7 @@ const DOTLESS_FURNITURE = Object.freeze(
|
|
|
721
721
|
* @returns {boolean}
|
|
722
722
|
*/
|
|
723
723
|
export function exemptFromUnsupportedLanguage(sourceFile) {
|
|
724
|
+
// used by its own test
|
|
724
725
|
const base = sourceFile.slice(sourceFile.lastIndexOf("/") + 1);
|
|
725
726
|
// Dotfiles are editor and tool state (`.gitignore`, `.env`, `.npmrc`).
|
|
726
727
|
if (base.startsWith(".")) return true;
|