@shrkcrft/boundaries 0.1.0-alpha.30 → 0.1.0-alpha.31
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/dist/baseline/compute-baseline.d.ts +8 -0
- package/dist/baseline/compute-baseline.d.ts.map +1 -1
- package/dist/baseline/compute-baseline.js +8 -7
- package/dist/baseline/diff-baseline.d.ts +28 -0
- package/dist/baseline/diff-baseline.d.ts.map +1 -1
- package/dist/baseline/diff-baseline.js +29 -0
- package/dist/evaluate/boundary-unit-finding.d.ts +10 -0
- package/dist/evaluate/boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-finding.js +21 -0
- package/dist/evaluate/boundary-unit-kind.d.ts +7 -0
- package/dist/evaluate/boundary-unit-kind.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-kind.js +14 -0
- package/dist/evaluate/evaluate-boundaries.d.ts +230 -3
- package/dist/evaluate/evaluate-boundaries.d.ts.map +1 -1
- package/dist/evaluate/evaluate-boundaries.js +481 -53
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts +27 -0
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settle-input.js +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts +23 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.js +1 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts +22 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-unit-finding.js +1 -0
- package/dist/evaluate/settle-boundary-rule.d.ts +23 -0
- package/dist/evaluate/settle-boundary-rule.d.ts.map +1 -0
- package/dist/evaluate/settle-boundary-rule.js +73 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts +11 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/with-boundary-rule-settlement.js +44 -0
- package/dist/extract/code-zones.d.ts +95 -3
- package/dist/extract/code-zones.d.ts.map +1 -1
- package/dist/extract/code-zones.js +313 -4
- package/dist/extract/extract-tokens.d.ts +30 -0
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +233 -27
- package/dist/extract/i-labeled-source.d.ts +12 -0
- package/dist/extract/i-labeled-source.d.ts.map +1 -0
- package/dist/extract/i-labeled-source.js +1 -0
- package/dist/extract/import-edges.d.ts +1 -0
- package/dist/extract/import-edges.d.ts.map +1 -1
- package/dist/extract/import-edges.js +20 -7
- package/dist/extract/inspect-source.d.ts +32 -3
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +50 -7
- package/dist/extract/parse-imports.d.ts +34 -12
- package/dist/extract/parse-imports.d.ts.map +1 -1
- package/dist/extract/parse-imports.js +119 -20
- package/dist/extract/scan-literals.d.ts +12 -1
- package/dist/extract/scan-literals.d.ts.map +1 -1
- package/dist/extract/scan-literals.js +15 -1
- package/dist/extract/source-liveness-request.d.ts +24 -0
- package/dist/extract/source-liveness-request.d.ts.map +1 -0
- package/dist/extract/source-liveness-request.js +45 -0
- package/dist/generated/check-provenance.d.ts.map +1 -1
- package/dist/generated/check-provenance.js +3 -1
- package/dist/generated/read-regen-tree.d.ts +16 -0
- package/dist/generated/read-regen-tree.d.ts.map +1 -0
- package/dist/generated/read-regen-tree.js +61 -0
- package/dist/generated/scan-generated.d.ts +8 -0
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +25 -10
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -0
- package/dist/model/boundary-intended-empty-line.d.ts +10 -0
- package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
- package/dist/model/boundary-intended-empty-line.js +11 -0
- package/dist/model/boundary-markable-list.d.ts +15 -0
- package/dist/model/boundary-markable-list.d.ts.map +1 -0
- package/dist/model/boundary-markable-list.js +15 -0
- package/dist/model/boundary-rule-input-keys.d.ts +11 -0
- package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
- package/dist/model/boundary-rule-input-keys.js +29 -0
- package/dist/model/boundary-rule-input.d.ts +23 -0
- package/dist/model/boundary-rule-input.d.ts.map +1 -0
- package/dist/model/boundary-rule-input.js +1 -0
- package/dist/model/boundary-rule-key-problems.d.ts +11 -0
- package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-key-problems.js +36 -0
- package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
- package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-marker-problems.js +94 -0
- package/dist/model/boundary-rule-scope.d.ts +88 -0
- package/dist/model/boundary-rule-scope.d.ts.map +1 -0
- package/dist/model/boundary-rule-scope.js +175 -0
- package/dist/model/boundary-rule.d.ts +120 -5
- package/dist/model/boundary-rule.d.ts.map +1 -1
- package/dist/model/boundary-rule.js +148 -1
- package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
- package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
- package/dist/model/boundary-unit-problem-issue.js +16 -0
- package/dist/model/normalize-boundary-rule.d.ts +21 -0
- package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
- package/dist/model/normalize-boundary-rule.js +62 -0
- package/dist/policy/evaluate-policy.d.ts +80 -3
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +152 -19
- package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
- package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
- package/dist/policy/i-policy-rule-liveness.js +1 -0
- package/dist/policy/run-policy.d.ts +1 -1
- package/dist/policy/run-policy.d.ts.map +1 -1
- package/dist/policy/run-policy.js +171 -21
- package/dist/registry/load-boundary-rules.d.ts +29 -1
- package/dist/registry/load-boundary-rules.d.ts.map +1 -1
- package/dist/registry/load-boundary-rules.js +38 -8
- package/dist/scan/glob.d.ts +90 -0
- package/dist/scan/glob.d.ts.map +1 -1
- package/dist/scan/glob.js +205 -0
- package/dist/scan/i-glob-unit-measure.d.ts +22 -0
- package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
- package/dist/scan/i-glob-unit-measure.js +1 -0
- package/dist/scan/import-pattern.d.ts +109 -0
- package/dist/scan/import-pattern.d.ts.map +1 -0
- package/dist/scan/import-pattern.js +191 -0
- package/dist/scan/node-builtin-package-names.d.ts +13 -0
- package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
- package/dist/scan/node-builtin-package-names.js +25 -0
- package/dist/scan/scan-imports.d.ts +36 -3
- package/dist/scan/scan-imports.d.ts.map +1 -1
- package/dist/scan/scan-imports.js +114 -45
- package/dist/util/blank-run-hazard-finding.d.ts +34 -0
- package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
- package/dist/util/blank-run-hazard-finding.js +1 -0
- package/dist/util/blank-run-hazard.d.ts +29 -0
- package/dist/util/blank-run-hazard.d.ts.map +1 -0
- package/dist/util/blank-run-hazard.js +450 -0
- package/dist/util/dead-glob-units.d.ts +46 -0
- package/dist/util/dead-glob-units.d.ts.map +1 -0
- package/dist/util/dead-glob-units.js +116 -0
- package/dist/util/glob-list-liveness-input.d.ts +31 -0
- package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
- package/dist/util/glob-list-liveness-input.js +65 -0
- package/dist/util/i-dead-glob-unit.d.ts +24 -0
- package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
- package/dist/util/i-dead-glob-unit.js +1 -0
- package/dist/util/i-glob-list-units.d.ts +25 -0
- package/dist/util/i-glob-list-units.d.ts.map +1 -0
- package/dist/util/i-glob-list-units.js +1 -0
- package/dist/util/i-glob-liveness-list.d.ts +23 -0
- package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-list.js +1 -0
- package/dist/util/i-glob-liveness-request.d.ts +11 -0
- package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-request.js +1 -0
- package/dist/util/i-glob-negation.d.ts +13 -0
- package/dist/util/i-glob-negation.d.ts.map +1 -0
- package/dist/util/i-glob-negation.js +1 -0
- package/dist/util/matched-files.d.ts +22 -0
- package/dist/util/matched-files.d.ts.map +1 -0
- package/dist/util/matched-files.js +1 -0
- package/dist/util/negation-cause.d.ts +15 -0
- package/dist/util/negation-cause.d.ts.map +1 -0
- package/dist/util/negation-cause.js +21 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
- package/dist/util/plane-scan-exclude-dirs.js +22 -0
- package/dist/util/read-glob-list-liveness.d.ts +20 -0
- package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
- package/dist/util/read-glob-list-liveness.js +20 -0
- package/dist/util/read-scope-coverage.d.ts +90 -0
- package/dist/util/read-scope-coverage.d.ts.map +1 -0
- package/dist/util/read-scope-coverage.js +173 -0
- package/dist/util/read-scope.d.ts +14 -0
- package/dist/util/read-scope.d.ts.map +1 -0
- package/dist/util/read-scope.js +1 -0
- package/dist/util/read-selected-files.d.ts +16 -0
- package/dist/util/read-selected-files.d.ts.map +1 -0
- package/dist/util/read-selected-files.js +25 -0
- package/dist/util/settle-glob-lists.d.ts +12 -0
- package/dist/util/settle-glob-lists.d.ts.map +1 -0
- package/dist/util/settle-glob-lists.js +13 -0
- package/dist/util/unread-file-reason.d.ts +26 -0
- package/dist/util/unread-file-reason.d.ts.map +1 -0
- package/dist/util/unread-file-reason.js +26 -0
- package/dist/util/unread-file.d.ts +10 -0
- package/dist/util/unread-file.d.ts.map +1 -0
- package/dist/util/unread-file.js +1 -0
- package/dist/util/walk-files.d.ts +74 -10
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +137 -31
- package/dist/wiring/evaluate-wiring.d.ts +79 -5
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +267 -12
- package/dist/wiring/explain-wiring.d.ts +49 -5
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +118 -10
- package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
- package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/i-idiom-role-coverage.js +1 -0
- package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
- package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/i-registration-query-verdict.js +1 -0
- package/dist/wiring/i-registration-roles.d.ts +71 -0
- package/dist/wiring/i-registration-roles.d.ts.map +1 -0
- package/dist/wiring/i-registration-roles.js +1 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/measure-idiom-role-coverage.js +20 -0
- package/dist/wiring/measure-registration-roles.d.ts +25 -0
- package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
- package/dist/wiring/measure-registration-roles.js +124 -0
- package/dist/wiring/plan-wiring-fix.js +2 -2
- package/dist/wiring/registration-graph.d.ts +12 -0
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +16 -5
- package/dist/wiring/registration-query-verdict.d.ts +23 -0
- package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/registration-query-verdict.js +118 -0
- package/dist/wiring/registry-query.d.ts +8 -0
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +12 -5
- package/dist/wiring/scan-wiring-files.d.ts +4 -2
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +52 -15
- package/dist/wiring/sink-imports.d.ts +11 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -1
- package/dist/wiring/sink-imports.js +11 -2
- package/dist/wiring/trace-literal.d.ts +6 -0
- package/dist/wiring/trace-literal.d.ts.map +1 -1
- package/dist/wiring/trace-literal.js +5 -2
- package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
- package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
- package/dist/wiring/wiring-labeled-sources.js +20 -0
- package/package.json +2 -2
|
@@ -1,95 +1,523 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UnitDeadCause, UnitDeadWeight, unitProblemsOf, } from '@shrkcrft/core';
|
|
2
|
+
import { BoundaryMarkableList } from "../model/boundary-markable-list.js";
|
|
3
|
+
import { boundaryForbiddenMatch, boundaryPatternOverlaps, boundaryRuleScope, boundaryRuleSeverity, boundaryScopeDecision, } from "../model/boundary-rule-scope.js";
|
|
4
|
+
import { normalizeBoundaryRule } from "../model/normalize-boundary-rule.js";
|
|
5
|
+
import { globToRegex, matchesAny } from "../scan/glob.js";
|
|
6
|
+
import { couldMatchPackageName, importPatternDefect, LEADING_WILDCARD_NEVER_DEAD, matchImportPattern, RELATIVE_PATTERN_NEVER_DEAD, } from "../scan/import-pattern.js";
|
|
2
7
|
import { resolveAliasCandidates, } from "../scan/tsconfig-aliases.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
import { settleBoundaryRule } from "./settle-boundary-rule.js";
|
|
9
|
+
import { withBoundaryRuleSettlement } from "./with-boundary-rule-settlement.js";
|
|
10
|
+
/** The skip reason an unnormalisable rule carries — the loaders' errored-rule words. */
|
|
11
|
+
const NOT_EVALUATED = 'failed validation — NOT evaluated';
|
|
6
12
|
/**
|
|
7
13
|
* Evaluate every rule against every scanned edge. Returns the list of
|
|
8
|
-
* violations + counts
|
|
14
|
+
* violations + counts, and — round 11 — what every rule and every selector
|
|
15
|
+
* unit actually matched, so a rule that enforced nothing can never read as
|
|
16
|
+
* evaluated. Pure function — no I/O.
|
|
17
|
+
*
|
|
18
|
+
* Round 13: each rule is normalised IDEMPOTENTLY at entry
|
|
19
|
+
* (`normalizeBoundaryRule` — a loaded rule comes back unchanged), so a
|
|
20
|
+
* hand-built rule with a `{ pattern, expectEmpty: true }` entry is honoured,
|
|
21
|
+
* never a `pattern.includes is not a function` crash; and every rule's units
|
|
22
|
+
* and emptiness settle through the ONE boundary settle (`settleBoundaryRule`).
|
|
9
23
|
*/
|
|
10
24
|
export function evaluateBoundaries(scan, rules, options = {}) {
|
|
11
|
-
const
|
|
25
|
+
const selected = options.onlyRuleId
|
|
12
26
|
? rules.filter((r) => r.id === options.onlyRuleId)
|
|
13
27
|
: rules;
|
|
28
|
+
const fileUniverse = scan.files ? 'scan' : 'edges';
|
|
29
|
+
const files = scan.files ?? [...new Set(scan.edges.map((e) => e.from))];
|
|
30
|
+
const edgesByFile = new Map();
|
|
31
|
+
for (const e of scan.edges) {
|
|
32
|
+
const list = edgesByFile.get(e.from);
|
|
33
|
+
if (list)
|
|
34
|
+
list.push(e);
|
|
35
|
+
else
|
|
36
|
+
edgesByFile.set(e.from, [e]);
|
|
37
|
+
}
|
|
38
|
+
// Literal specifier + tsconfig alias resolutions, computed once per specifier.
|
|
39
|
+
const candidateCache = new Map();
|
|
40
|
+
const candidatesOf = (specifier) => {
|
|
41
|
+
const hit = candidateCache.get(specifier);
|
|
42
|
+
if (hit)
|
|
43
|
+
return hit;
|
|
44
|
+
const out = [specifier];
|
|
45
|
+
if (options.tsconfigPaths) {
|
|
46
|
+
for (const resolved of resolveAliasCandidates(specifier, options.tsconfigPaths))
|
|
47
|
+
out.push(resolved);
|
|
48
|
+
}
|
|
49
|
+
candidateCache.set(specifier, out);
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
52
|
+
const distinctSpecifiers = [...new Set(scan.edges.map((e) => e.importSpecifier))];
|
|
53
|
+
const anywhere = new PatternReach(distinctSpecifiers, candidatesOf, files, options);
|
|
14
54
|
const violations = [];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
55
|
+
const suppressed = [];
|
|
56
|
+
const staleExceptions = [];
|
|
57
|
+
const coverage = [];
|
|
58
|
+
const invalidRules = [];
|
|
59
|
+
for (const input of selected) {
|
|
60
|
+
const normalized = normalizeBoundaryRule(input);
|
|
61
|
+
if (!normalized.ok) {
|
|
62
|
+
const problems = unitProblemsOf(normalized.error);
|
|
63
|
+
const ruleId = typeof input.id === 'string' ? input.id : '(no id)';
|
|
64
|
+
invalidRules.push({ ruleId, problems });
|
|
65
|
+
coverage.push({
|
|
66
|
+
ruleId,
|
|
67
|
+
severity: boundaryRuleSeverity(input),
|
|
68
|
+
status: 'skipped',
|
|
69
|
+
skipReason: `${NOT_EVALUATED}: ${problems.join('; ')}`,
|
|
70
|
+
failedOnEmpty: true,
|
|
71
|
+
filesInScope: 0,
|
|
72
|
+
exemptFilesInScope: 0,
|
|
73
|
+
edgesInScope: 0,
|
|
74
|
+
violations: 0,
|
|
75
|
+
suppressed: 0,
|
|
76
|
+
fromGlobs: [],
|
|
77
|
+
forbidden: [],
|
|
78
|
+
allowed: [],
|
|
79
|
+
exemptions: [],
|
|
80
|
+
exceptions: [],
|
|
81
|
+
deadUnits: [],
|
|
82
|
+
coverage: { unit: 'rules', expected: 1, examined: 0, reason: NOT_EVALUATED },
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const rule = normalized.value;
|
|
87
|
+
const scope = boundaryRuleScope(rule);
|
|
88
|
+
const mode = boundaryForbiddenMatch(rule);
|
|
89
|
+
const severity = boundaryRuleSeverity(rule);
|
|
90
|
+
const forbiddenPatterns = rule.forbiddenImports ?? [];
|
|
91
|
+
const allowedPatterns = rule.allowedImports ?? [];
|
|
92
|
+
const exceptions = rule.exceptions ?? [];
|
|
93
|
+
const governedPerGlob = new Map(scope.include.map((g) => [g, 0]));
|
|
94
|
+
const anyPerGlob = new Map(scope.include.map((g) => [g, 0]));
|
|
95
|
+
const exemptPerIndex = scope.exemptions.map(() => 0);
|
|
96
|
+
const hitsInScope = new Map(forbiddenPatterns.map((p) => [p, 0]));
|
|
97
|
+
const exceptionMatched = exceptions.map(() => 0);
|
|
98
|
+
let filesInScope = 0;
|
|
99
|
+
let exemptFilesInScope = 0;
|
|
100
|
+
let edgesInScope = 0;
|
|
101
|
+
let ruleViolations = 0;
|
|
102
|
+
let ruleSuppressed = 0;
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
// THE scope decision — the same function why-file, the rule-graph bridge
|
|
105
|
+
// and every other reader call, so none of them can disagree with this.
|
|
106
|
+
const decision = boundaryScopeDecision(scope, file);
|
|
107
|
+
if (decision === 'out')
|
|
19
108
|
continue;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
109
|
+
const matchedInclude = scope.include.filter((g) => globToRegex(g).test(file));
|
|
110
|
+
for (const g of matchedInclude)
|
|
111
|
+
anyPerGlob.set(g, (anyPerGlob.get(g) ?? 0) + 1);
|
|
112
|
+
let exemptGlob;
|
|
113
|
+
if (decision === 'exempt') {
|
|
114
|
+
exemptFilesInScope += 1;
|
|
115
|
+
scope.exemptions.forEach((e, i) => {
|
|
116
|
+
if (globToRegex(e.glob).test(file)) {
|
|
117
|
+
exemptPerIndex[i] = (exemptPerIndex[i] ?? 0) + 1;
|
|
118
|
+
exemptGlob ??= e.glob;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
filesInScope += 1;
|
|
124
|
+
for (const g of matchedInclude)
|
|
125
|
+
governedPerGlob.set(g, (governedPerGlob.get(g) ?? 0) + 1);
|
|
29
126
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
127
|
+
const matchedFrom = matchedInclude[0] ?? scope.include[0] ?? '';
|
|
128
|
+
for (const edge of edgesByFile.get(file) ?? []) {
|
|
129
|
+
if (decision === 'in')
|
|
130
|
+
edgesInScope += 1;
|
|
131
|
+
const specifiers = candidatesOf(edge.importSpecifier);
|
|
132
|
+
let violation;
|
|
133
|
+
// Forbidden imports — the first candidate any pattern reaches wins.
|
|
34
134
|
for (const spec of specifiers) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
135
|
+
let hit;
|
|
136
|
+
for (const p of forbiddenPatterns) {
|
|
137
|
+
const kind = matchImportPattern(spec, p, mode);
|
|
138
|
+
if (kind) {
|
|
139
|
+
hit = { pattern: p, kind };
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (hit) {
|
|
144
|
+
violation = violationFor(rule, severity, edge, matchedFrom, hit.pattern, hit.kind);
|
|
145
|
+
if (spec !== edge.importSpecifier)
|
|
146
|
+
violation.resolvedVia = spec;
|
|
147
|
+
if (decision === 'in')
|
|
148
|
+
hitsInScope.set(hit.pattern, (hitsInScope.get(hit.pattern) ?? 0) + 1);
|
|
39
149
|
break;
|
|
40
150
|
}
|
|
41
151
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
152
|
+
// Allowed imports — when set, anything NOT matching (any candidate) is
|
|
153
|
+
// a violation. Exact glob semantics: an allow-list is never widened.
|
|
154
|
+
if (!violation && allowedPatterns.length > 0) {
|
|
155
|
+
const matchedAllowed = specifiers.some((s) => matchesAny(s, allowedPatterns));
|
|
156
|
+
if (!matchedAllowed && !edge.importSpecifier.startsWith('.')) {
|
|
157
|
+
violation = { ...violationFor(rule, severity, edge, matchedFrom, undefined, undefined), notAllowed: true };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!violation)
|
|
161
|
+
continue;
|
|
162
|
+
if (decision === 'exempt') {
|
|
163
|
+
suppressed.push({ violation, reason: 'exempt-file', ...(exemptGlob ? { glob: exemptGlob } : {}) });
|
|
164
|
+
ruleSuppressed += 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Credit EVERY exception whose (path, target) matches this edge, and
|
|
168
|
+
// suppress the violation once (attributed to the first). Crediting
|
|
169
|
+
// only the first made an overlapping exception — an area-wide one next
|
|
170
|
+
// to a file-level one for the same edge — read as STALE (exit 1) while
|
|
171
|
+
// it matched a real edge. Only an exception matching no violating edge
|
|
172
|
+
// at all is stale.
|
|
173
|
+
let exceptionIndex = -1;
|
|
174
|
+
exceptions.forEach((ex, idx) => {
|
|
175
|
+
if (!matchesAny(edge.from, [ex.path]))
|
|
176
|
+
return;
|
|
177
|
+
if (!specifiers.some((s) => matchImportPattern(s, ex.target, mode) !== null))
|
|
178
|
+
return;
|
|
179
|
+
exceptionMatched[idx] = (exceptionMatched[idx] ?? 0) + 1;
|
|
180
|
+
if (exceptionIndex < 0)
|
|
181
|
+
exceptionIndex = idx;
|
|
182
|
+
});
|
|
183
|
+
if (exceptionIndex >= 0) {
|
|
184
|
+
suppressed.push({ violation, reason: 'exception', exception: exceptions[exceptionIndex] });
|
|
185
|
+
ruleSuppressed += 1;
|
|
186
|
+
continue;
|
|
59
187
|
}
|
|
188
|
+
violations.push(violation);
|
|
189
|
+
ruleViolations += 1;
|
|
60
190
|
}
|
|
61
191
|
}
|
|
192
|
+
// ── selector units: OBSERVE, then settle through the one boundary settle ──
|
|
193
|
+
// Each unit reports two facts (round 13): whether its target EXISTS at all
|
|
194
|
+
// (raw — before exemptions and effectiveness), which decides a MARKED unit
|
|
195
|
+
// (intended-empty or went-live); and whether it is LIVE (the dead predicate
|
|
196
|
+
// this plane has always used, negated), which decides an unmarked one.
|
|
197
|
+
const includedAny = filesInScope + exemptFilesInScope;
|
|
198
|
+
const marked = new Set((rule.expectEmptyUnits ?? []).map((m) => `${m.list} ${m.unit}`));
|
|
199
|
+
const observations = [];
|
|
200
|
+
for (const g of scope.include) {
|
|
201
|
+
const any = anyPerGlob.get(g) ?? 0;
|
|
202
|
+
const governed = governedPerGlob.get(g) ?? 0;
|
|
203
|
+
observations.push({
|
|
204
|
+
list: BoundaryMarkableList.From,
|
|
205
|
+
unit: g,
|
|
206
|
+
label: g,
|
|
207
|
+
weight: UnitDeadWeight.Coverage,
|
|
208
|
+
exists: any > 0,
|
|
209
|
+
live: governed > 0,
|
|
210
|
+
matched: any,
|
|
211
|
+
liveBecause: governed > 0 ? `it governs ${governed} scanned file(s)` : `it matches ${any} scanned file(s)`,
|
|
212
|
+
deadReason: any > 0 ? `matches only exempt files (${any})` : `matched 0 of ${files.length} scanned files`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
scope.exemptions.forEach((e, i) => {
|
|
216
|
+
// The `excludeTests` shorthand is allowed to be vacuous (a repo with no
|
|
217
|
+
// tests yet). An exemption of a rule whose from globs match nothing
|
|
218
|
+
// exempts nothing by construction, so it is judged only when marked —
|
|
219
|
+
// a marker is never left unobserved.
|
|
220
|
+
if (e.origin === 'excludeTests')
|
|
221
|
+
return;
|
|
222
|
+
const list = e.origin === 'from-negation' ? BoundaryMarkableList.From : BoundaryMarkableList.ExemptFiles;
|
|
223
|
+
const unit = e.origin === 'from-negation' ? `!${e.glob}` : e.glob;
|
|
224
|
+
if (includedAny === 0 && !marked.has(`${list} ${unit}`))
|
|
225
|
+
return;
|
|
226
|
+
const n = exemptPerIndex[i] ?? 0;
|
|
227
|
+
observations.push({
|
|
228
|
+
list,
|
|
229
|
+
unit,
|
|
230
|
+
label: unit,
|
|
231
|
+
exists: n > 0,
|
|
232
|
+
live: n > 0,
|
|
233
|
+
matched: n,
|
|
234
|
+
liveBecause: `it exempts ${n} file(s)`,
|
|
235
|
+
deadReason: includedAny > 0
|
|
236
|
+
? `exempts none of the ${includedAny} file(s) the rule's from globs match`
|
|
237
|
+
: "exempts nothing — the rule's from globs match no file",
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// Patterns that can never change the verdict — the one static answer
|
|
241
|
+
// `boundaries explain` and MCP `get_boundary_rule` print too.
|
|
242
|
+
const overlaps = boundaryPatternOverlaps(rule);
|
|
243
|
+
const subsumedBy = new Map(overlaps.redundantForbidden.map((o) => [o.index, o.by]));
|
|
244
|
+
const shadowedBy = new Map(overlaps.shadowedAllowed.map((o) => [o.index, o.by]));
|
|
245
|
+
const forbiddenCounts = forbiddenPatterns.map((p, i) => {
|
|
246
|
+
const reach = anywhere.forbidden(p, mode);
|
|
247
|
+
const resolvedBy = reach.hits > 0 ? undefined : anywhere.resolvedBy(p, mode);
|
|
248
|
+
const resolvable = reach.hits > 0 || resolvedBy !== undefined;
|
|
249
|
+
// A malformed pattern says WHY (the validator rejects it at load; a
|
|
250
|
+
// hand-built rule reaches here) — never the "typo, retired target" causes.
|
|
251
|
+
const defect = resolvable ? undefined : importPatternDefect(p, mode);
|
|
252
|
+
const subpathNote = mode === 'exact' && reach.subpathHits > 0
|
|
253
|
+
? ` — ${reach.subpathHits} subpath import(s) of it exist; forbiddenMatch: 'exact' excludes them`
|
|
254
|
+
: '';
|
|
255
|
+
observations.push({
|
|
256
|
+
list: BoundaryMarkableList.ForbiddenImports,
|
|
257
|
+
unit: p,
|
|
258
|
+
label: p,
|
|
259
|
+
exists: resolvable,
|
|
260
|
+
live: resolvable,
|
|
261
|
+
matched: reach.hits,
|
|
262
|
+
liveBecause: reach.hits > 0 ? `${reach.hits} import(s) match it` : `no import yet, but ${resolvedBy ?? 'it resolves'}`,
|
|
263
|
+
deadReason: defect ??
|
|
264
|
+
`matches no import anywhere in the repo, no workspace/dependency package name, no tsconfig alias and no file${subpathNote}`,
|
|
265
|
+
...(defect !== undefined ? { cause: UnitDeadCause.Defect } : {}),
|
|
266
|
+
});
|
|
267
|
+
const coveredBy = subsumedBy.get(i);
|
|
268
|
+
return {
|
|
269
|
+
pattern: p,
|
|
270
|
+
hitsInScope: hitsInScope.get(p) ?? 0,
|
|
271
|
+
hitsAnywhere: reach.hits,
|
|
272
|
+
resolvable,
|
|
273
|
+
...(mode === 'exact' ? { subpathHitsAnywhere: reach.subpathHits } : {}),
|
|
274
|
+
...(coveredBy !== undefined ? { subsumedBy: coveredBy } : {}),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
const allowedCounts = allowedPatterns.map((p, i) => {
|
|
278
|
+
const hits = anywhere.allowed(p);
|
|
279
|
+
const shadow = shadowedBy.get(i);
|
|
280
|
+
if (shadow !== undefined) {
|
|
281
|
+
// Forbidden is checked first and allowed never re-admits: an allowance
|
|
282
|
+
// a forbidden pattern covers can never admit anything — the carve-out
|
|
283
|
+
// under a bare forbidden package that package semantics made inert.
|
|
284
|
+
observations.push({
|
|
285
|
+
list: BoundaryMarkableList.AllowedImports,
|
|
286
|
+
unit: p,
|
|
287
|
+
label: p,
|
|
288
|
+
exists: hits > 0,
|
|
289
|
+
live: false,
|
|
290
|
+
matched: hits,
|
|
291
|
+
deadReason: `shadowed by forbidden '${shadow}' — forbiddenImports is checked first` +
|
|
292
|
+
(mode === 'package' ? ` and '${shadow}' covers every subpath (package semantics)` : '') +
|
|
293
|
+
`, so this allowance can never admit an import; carve the subpath out with exceptions[{ path, target: '${p}', reason }]` +
|
|
294
|
+
(mode === 'package' ? " or set forbiddenMatch: 'exact'" : ''),
|
|
295
|
+
cause: UnitDeadCause.Shadowed,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const resolvedBy = hits > 0 ? undefined : anywhere.resolvedBy(p, 'exact');
|
|
300
|
+
const resolvable = hits > 0 || resolvedBy !== undefined;
|
|
301
|
+
const defect = resolvable ? undefined : importPatternDefect(p, 'exact');
|
|
302
|
+
observations.push({
|
|
303
|
+
list: BoundaryMarkableList.AllowedImports,
|
|
304
|
+
unit: p,
|
|
305
|
+
label: p,
|
|
306
|
+
exists: resolvable,
|
|
307
|
+
live: resolvable,
|
|
308
|
+
matched: hits,
|
|
309
|
+
liveBecause: hits > 0 ? `${hits} import(s) match it` : `no import yet, but ${resolvedBy ?? 'it resolves'}`,
|
|
310
|
+
deadReason: defect ??
|
|
311
|
+
'matches no import anywhere in the repo, no workspace/dependency package name, no tsconfig alias and no file',
|
|
312
|
+
...(defect !== undefined ? { cause: UnitDeadCause.Defect } : {}),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return { pattern: p, hitsAnywhere: hits, ...(shadow !== undefined ? { shadowedBy: shadow } : {}) };
|
|
316
|
+
});
|
|
317
|
+
// ── stale exceptions ──────────────────────────────────────────────────
|
|
318
|
+
exceptions.forEach((ex, index) => {
|
|
319
|
+
if ((exceptionMatched[index] ?? 0) > 0)
|
|
320
|
+
return;
|
|
321
|
+
staleExceptions.push({
|
|
322
|
+
ruleId: rule.id,
|
|
323
|
+
index,
|
|
324
|
+
exception: ex,
|
|
325
|
+
severity: 'error',
|
|
326
|
+
message: `exception ${index} of ${rule.id} (path ${ex.path} → target ${ex.target}; reason: ${ex.reason}) no longer matches any violating edge — delete it, or the rule it narrows is not what you think`,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
// ── status + coverage: THE boundary settle ────────────────────────────
|
|
330
|
+
const settlement = settleBoundaryRule({
|
|
331
|
+
rule,
|
|
332
|
+
observations,
|
|
333
|
+
includeGlobs: scope.include,
|
|
334
|
+
filesScanned: files.length,
|
|
335
|
+
fileUniverse,
|
|
336
|
+
filesInScope,
|
|
337
|
+
exemptFilesInScope,
|
|
338
|
+
violations: ruleViolations,
|
|
339
|
+
unread: false,
|
|
340
|
+
});
|
|
341
|
+
coverage.push(withBoundaryRuleSettlement({
|
|
342
|
+
ruleId: rule.id,
|
|
343
|
+
severity,
|
|
344
|
+
filesInScope,
|
|
345
|
+
exemptFilesInScope,
|
|
346
|
+
edgesInScope,
|
|
347
|
+
violations: ruleViolations,
|
|
348
|
+
suppressed: ruleSuppressed,
|
|
349
|
+
fromGlobs: scope.include.map((g) => ({ glob: g, files: governedPerGlob.get(g) ?? 0 })),
|
|
350
|
+
forbidden: forbiddenCounts,
|
|
351
|
+
allowed: allowedCounts,
|
|
352
|
+
exemptions: scope.exemptions.map((e, i) => ({ ...e, files: exemptPerIndex[i] ?? 0 })),
|
|
353
|
+
exceptions: exceptions.map((ex, i) => ({ ...ex, matched: exceptionMatched[i] ?? 0 })),
|
|
354
|
+
}, settlement));
|
|
62
355
|
}
|
|
63
356
|
const counts = { error: 0, warning: 0, info: 0 };
|
|
64
357
|
for (const v of violations)
|
|
65
358
|
counts[v.severity] += 1;
|
|
359
|
+
// Every rule's settled dead list, in rule order — read, never re-derived.
|
|
360
|
+
const deadUnits = coverage.flatMap((c) => c.deadUnits);
|
|
66
361
|
return {
|
|
67
|
-
|
|
362
|
+
rulesConfigured: selected.length,
|
|
363
|
+
rulesEvaluated: coverage.filter((c) => c.status !== 'skipped' && c.acceptedAsIntendedEmpty !== true).length,
|
|
364
|
+
rulesAcceptedEmpty: coverage.filter((c) => c.acceptedAsIntendedEmpty === true).length,
|
|
68
365
|
edgesEvaluated: scan.edges.length,
|
|
366
|
+
filesScanned: files.length,
|
|
367
|
+
fileUniverse,
|
|
69
368
|
violations,
|
|
70
369
|
counts,
|
|
370
|
+
suppressed,
|
|
371
|
+
suppressedCounts: {
|
|
372
|
+
exemptFile: suppressed.filter((s) => s.reason === 'exempt-file').length,
|
|
373
|
+
exception: suppressed.filter((s) => s.reason === 'exception').length,
|
|
374
|
+
},
|
|
375
|
+
staleExceptions,
|
|
376
|
+
coverage,
|
|
377
|
+
skipped: coverage
|
|
378
|
+
.filter((c) => c.status === 'skipped')
|
|
379
|
+
.map((c) => ({
|
|
380
|
+
ruleId: c.ruleId,
|
|
381
|
+
reason: c.skipReason ?? 'checked nothing',
|
|
382
|
+
severity: c.severity,
|
|
383
|
+
failed: c.failedOnEmpty === true,
|
|
384
|
+
})),
|
|
385
|
+
deadUnits,
|
|
386
|
+
...(invalidRules.length > 0 ? { invalidRules } : {}),
|
|
71
387
|
};
|
|
72
388
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
389
|
+
/**
|
|
390
|
+
* "Could this pattern ever match?" — the whole-scan reach of a pattern,
|
|
391
|
+
* memoised per (pattern, mode) because rule sets share patterns.
|
|
392
|
+
*
|
|
393
|
+
* 0 violations in scope is the NORMAL state of a healthy fence, so a pattern is
|
|
394
|
+
* dead only when it resolves to nothing ANYWHERE: no import in the repo, no
|
|
395
|
+
* workspace / dependency / builtin package name, no tsconfig alias, no file.
|
|
396
|
+
*/
|
|
397
|
+
class PatternReach {
|
|
398
|
+
specifiers;
|
|
399
|
+
candidatesOf;
|
|
400
|
+
files;
|
|
401
|
+
options;
|
|
402
|
+
forbiddenCache = new Map();
|
|
403
|
+
allowedCache = new Map();
|
|
404
|
+
resolveCache = new Map();
|
|
405
|
+
constructor(specifiers, candidatesOf, files, options) {
|
|
406
|
+
this.specifiers = specifiers;
|
|
407
|
+
this.candidatesOf = candidatesOf;
|
|
408
|
+
this.files = files;
|
|
409
|
+
this.options = options;
|
|
410
|
+
}
|
|
411
|
+
forbidden(pattern, mode) {
|
|
412
|
+
const key = [mode, pattern].join(String.fromCharCode(0));
|
|
413
|
+
const cached = this.forbiddenCache.get(key);
|
|
414
|
+
if (cached)
|
|
415
|
+
return cached;
|
|
416
|
+
let hits = 0;
|
|
417
|
+
let subpathHits = 0;
|
|
418
|
+
for (const s of this.specifiers) {
|
|
419
|
+
const candidates = this.candidatesOf(s);
|
|
420
|
+
if (candidates.some((c) => matchImportPattern(c, pattern, mode) !== null)) {
|
|
421
|
+
hits += 1;
|
|
422
|
+
}
|
|
423
|
+
else if (mode === 'exact' &&
|
|
424
|
+
candidates.some((c) => matchImportPattern(c, pattern, 'package') === 'subpath')) {
|
|
425
|
+
subpathHits += 1;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const out = { hits, subpathHits };
|
|
429
|
+
this.forbiddenCache.set(key, out);
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
allowed(pattern) {
|
|
433
|
+
const cached = this.allowedCache.get(pattern);
|
|
434
|
+
if (cached !== undefined)
|
|
435
|
+
return cached;
|
|
436
|
+
let hits = 0;
|
|
437
|
+
for (const s of this.specifiers) {
|
|
438
|
+
if (this.candidatesOf(s).some((c) => matchesAny(c, [pattern])))
|
|
439
|
+
hits += 1;
|
|
440
|
+
}
|
|
441
|
+
this.allowedCache.set(pattern, hits);
|
|
442
|
+
return hits;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* WHY the pattern is resolvable with no import of it today — a relative
|
|
446
|
+
* target, a known package, an alias, a file — or `undefined` when nothing
|
|
447
|
+
* names it. The went-live line quotes it (round 13), so a stale marker says
|
|
448
|
+
* what made its target real.
|
|
449
|
+
*/
|
|
450
|
+
resolvedBy(pattern, mode) {
|
|
451
|
+
const key = [mode, pattern].join(String.fromCharCode(0));
|
|
452
|
+
if (this.resolveCache.has(key))
|
|
453
|
+
return this.resolveCache.get(key);
|
|
454
|
+
let out;
|
|
455
|
+
// A malformed pattern (`importPatternDefect`) cannot mean what it says —
|
|
456
|
+
// never "resolvable" through a package name it merely spells
|
|
457
|
+
// (`'@scope/pkg/'` used to read resolvable via the known `@scope/pkg`).
|
|
458
|
+
if (importPatternDefect(pattern, mode) === undefined) {
|
|
459
|
+
if (pattern.startsWith('.')) {
|
|
460
|
+
// A relative pattern cannot be judged without an importing file — so
|
|
461
|
+
// it is never reported dead, and a marker on it has nothing to wait for
|
|
462
|
+
// (the rule validator refuses one: `importPatternNeverJudgedDead`).
|
|
463
|
+
out = RELATIVE_PATTERN_NEVER_DEAD;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
// Name a piece of EVIDENCE only when it is one: a known package the
|
|
467
|
+
// pattern actually matches (or names a subpath / prefix of), an alias,
|
|
468
|
+
// a file. A LEADING `*` "could match" every package name — the
|
|
469
|
+
// permissive half of the dead predicate, never proof that the target
|
|
470
|
+
// exists — so it is said as what it is, never as "'<builtin>' is a
|
|
471
|
+
// known package" (round 13 review: `*-legacy` read "'_http_agent' is a
|
|
472
|
+
// known package"). The resolvable/not answer is unchanged.
|
|
473
|
+
const known = this.options.knownPackages ?? [];
|
|
474
|
+
const leadingWildcard = pattern.startsWith('*');
|
|
475
|
+
const pkg = known.find((k) => matchImportPattern(k, pattern, mode) !== null) ??
|
|
476
|
+
(leadingWildcard ? undefined : known.find((k) => couldMatchPackageName(pattern, k, mode)));
|
|
477
|
+
const alias = pkg === undefined
|
|
478
|
+
? [...(this.options.tsconfigPaths?.aliases.keys() ?? [])].find((aliasKey) => aliasKey.endsWith('*')
|
|
479
|
+
? pattern.startsWith(aliasKey.slice(0, -1)) ||
|
|
480
|
+
couldMatchPackageName(pattern, aliasKey.slice(0, -1) + 'x', mode)
|
|
481
|
+
: couldMatchPackageName(pattern, aliasKey, mode))
|
|
482
|
+
: undefined;
|
|
483
|
+
const file = pkg === undefined && alias === undefined
|
|
484
|
+
? this.files.find((f) => matchImportPattern(f, pattern, 'package') !== null)
|
|
485
|
+
: undefined;
|
|
486
|
+
const wildcardOnly = pkg === undefined &&
|
|
487
|
+
alias === undefined &&
|
|
488
|
+
file === undefined &&
|
|
489
|
+
leadingWildcard &&
|
|
490
|
+
known.some((k) => couldMatchPackageName(pattern, k, mode));
|
|
491
|
+
out =
|
|
492
|
+
pkg !== undefined
|
|
493
|
+
? `'${pkg}' is a known package (a node builtin, a workspace package or a declared dependency)`
|
|
494
|
+
: alias !== undefined
|
|
495
|
+
? `the tsconfig alias '${alias}' names it`
|
|
496
|
+
: file !== undefined
|
|
497
|
+
? `the file '${file}' matches it`
|
|
498
|
+
: wildcardOnly
|
|
499
|
+
? LEADING_WILDCARD_NEVER_DEAD
|
|
500
|
+
: undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
this.resolveCache.set(key, out);
|
|
504
|
+
return out;
|
|
77
505
|
}
|
|
78
|
-
return undefined;
|
|
79
506
|
}
|
|
80
|
-
function violationFor(rule, edge, matchedFrom, matchedForbidden) {
|
|
507
|
+
function violationFor(rule, severity, edge, matchedFrom, matchedForbidden, matchKind) {
|
|
81
508
|
return {
|
|
82
509
|
ruleId: rule.id,
|
|
83
510
|
ruleTitle: rule.title,
|
|
84
|
-
severity
|
|
511
|
+
severity,
|
|
85
512
|
file: edge.from,
|
|
86
513
|
importSpecifier: edge.importSpecifier,
|
|
87
514
|
line: edge.line,
|
|
88
515
|
matchedFrom,
|
|
89
516
|
...(matchedForbidden ? { matchedForbidden } : {}),
|
|
517
|
+
...(matchKind ? { matchKind } : {}),
|
|
90
518
|
message: rule.message ??
|
|
91
519
|
(matchedForbidden
|
|
92
|
-
? `Forbidden import in ${edge.from}: "${edge.importSpecifier}" matched "${matchedForbidden}"`
|
|
520
|
+
? `Forbidden import in ${edge.from}: "${edge.importSpecifier}" matched "${matchedForbidden}"${matchKind === 'subpath' ? ' (a subpath of the forbidden package)' : ''}`
|
|
93
521
|
: `Import "${edge.importSpecifier}" not in allowed list for ${rule.id}`),
|
|
94
522
|
...(rule.suggestedFix ? { suggestedFix: rule.suggestedFix } : {}),
|
|
95
523
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IUnitObservation } from '@shrkcrft/core';
|
|
2
|
+
import type { IBoundaryRule } from '../model/boundary-rule.js';
|
|
3
|
+
/** What `settleBoundaryRule` needs: one rule's selector-unit observations and the counts its emptiness is decided from. */
|
|
4
|
+
export interface IBoundaryRuleSettleInput {
|
|
5
|
+
readonly rule: Pick<IBoundaryRule, 'id' | 'severity' | 'failOnEmpty' | 'expectEmptyUnits'>;
|
|
6
|
+
/**
|
|
7
|
+
* One per judged unit: `from` inclusions (Coverage weight), `from` negations
|
|
8
|
+
* and `exemptFiles`, `forbiddenImports`, `allowedImports` (Advisory). The
|
|
9
|
+
* `list` of each is its `BoundaryMarkableList` value — the list its marks carry.
|
|
10
|
+
*/
|
|
11
|
+
readonly observations: readonly IUnitObservation[];
|
|
12
|
+
/** The rule's `from` inclusion globs — named when the rule matched no file. */
|
|
13
|
+
readonly includeGlobs: readonly string[];
|
|
14
|
+
/** Scanned files the scopes were counted against. */
|
|
15
|
+
readonly filesScanned: number;
|
|
16
|
+
/** Where that file list came from (`edges`: a hand-built scan without a file list). */
|
|
17
|
+
readonly fileUniverse: 'scan' | 'edges';
|
|
18
|
+
/** Governed files: inside `from`, not exempt. */
|
|
19
|
+
readonly filesInScope: number;
|
|
20
|
+
/** Inside `from` but exempt. */
|
|
21
|
+
readonly exemptFilesInScope: number;
|
|
22
|
+
/** The rule's unsuppressed violations. */
|
|
23
|
+
readonly violations: number;
|
|
24
|
+
/** A governed file in the rule's scope could not be read: the rule is partial, never "matched nothing". */
|
|
25
|
+
readonly unread: boolean;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=i-boundary-rule-settle-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i-boundary-rule-settle-input.d.ts","sourceRoot":"","sources":["../../src/evaluate/i-boundary-rule-settle-input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,2HAA2H;AAC3H,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,UAAU,GAAG,aAAa,GAAG,kBAAkB,CAAC,CAAC;IAC3F;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACnD,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,uFAAuF;IACvF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,iDAAiD;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gCAAgC;IAChC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2GAA2G;IAC3G,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ISettledRuleEmptiness, ISettledUnitLiveness, IUnitStateLists, IVerdictCoverage } from '@shrkcrft/core';
|
|
2
|
+
import type { BoundaryRuleStatus, IBoundaryDeadUnit } from './evaluate-boundaries.js';
|
|
3
|
+
/** `settleBoundaryRule`'s answer: how one rule settled — its units, its emptiness, and the fields its coverage record carries. */
|
|
4
|
+
export interface IBoundaryRuleSettlement {
|
|
5
|
+
/** Every unit's state (core `settleUnitLiveness`). */
|
|
6
|
+
readonly settled: ISettledUnitLiveness;
|
|
7
|
+
/** What the rule's empty scope is (core `settleRuleEmptiness`). */
|
|
8
|
+
readonly emptiness: ISettledRuleEmptiness;
|
|
9
|
+
readonly status: BoundaryRuleStatus;
|
|
10
|
+
/** Why the rule checked nothing, when `skipped`. */
|
|
11
|
+
readonly skipReason?: string;
|
|
12
|
+
/** A skip that fails the run (`failOnEmpty`). */
|
|
13
|
+
readonly failedOnEmpty: boolean;
|
|
14
|
+
/** The primary verdict record: the scope-glob shortfall (record A), or the acceptance itself for an IntendedEmpty rule. */
|
|
15
|
+
readonly coverage: IVerdictCoverage;
|
|
16
|
+
/** Record B — the rule's intended-empty units, `acceptedBy: 'expectEmpty'`. */
|
|
17
|
+
readonly unitAcceptance?: IVerdictCoverage;
|
|
18
|
+
/** The rule's dead / intended-empty / went-live unit lines (`unitStateLists`), when it has any. */
|
|
19
|
+
readonly units?: IUnitStateLists;
|
|
20
|
+
/** The settled `.dead` (UNMARKED units only), as the boundary JSON has always reported them. */
|
|
21
|
+
readonly deadUnits: readonly IBoundaryDeadUnit[];
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=i-boundary-rule-settlement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i-boundary-rule-settlement.d.ts","sourceRoot":"","sources":["../../src/evaluate/i-boundary-rule-settlement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAEtF,kIAAkI;AAClI,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,iDAAiD;IACjD,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,2HAA2H;IAC3H,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAC3C,mGAAmG;IACnG,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAClD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|