@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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { failsWhenEmpty, parseGlobList } from '@shrkcrft/core';
|
|
2
|
+
import { matchesAny } from "../scan/glob.js";
|
|
3
|
+
import { importPatternSubsumes } from "../scan/import-pattern.js";
|
|
4
|
+
import { unreadEntryMatches, unreadEntryWhollyMatches } from "../util/read-scope-coverage.js";
|
|
5
|
+
/**
|
|
6
|
+
* The ONE answer to "is file F in rule R's scope?" and "how severe is R?".
|
|
7
|
+
*
|
|
8
|
+
* Before round 11 four code paths each read `rule.from` on their own — the
|
|
9
|
+
* evaluator, `why-file`, the rule-graph bridge, the changes summary — and the
|
|
10
|
+
* CLI renderers each defaulted an unset severity to `'warning'` while the
|
|
11
|
+
* evaluator enforced it as `'error'` (an author inspecting a rule was told
|
|
12
|
+
* "warning" for a rule that blocks CI). With exemptions in the model, any
|
|
13
|
+
* reader that kept matching `from` alone would keep claiming a rule applies to
|
|
14
|
+
* an exempted file. So every reader calls these, and the evaluator itself
|
|
15
|
+
* decides scope through {@link boundaryScopeDecision}.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The test-file shorthand `excludeTests: true` expands to. Deliberately the
|
|
19
|
+
* common layouts only — a repo with another convention lists its own globs in
|
|
20
|
+
* `exemptFiles`.
|
|
21
|
+
*/
|
|
22
|
+
export const TEST_FILE_GLOBS = [
|
|
23
|
+
'**/__tests__/**',
|
|
24
|
+
'**/__mocks__/**',
|
|
25
|
+
'**/*.spec.*',
|
|
26
|
+
'**/*.test.*',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* A rule's source-side scope: the globs that include files and the globs that
|
|
30
|
+
* exempt them.
|
|
31
|
+
*
|
|
32
|
+
* A `from` entry starting with `!` is normalised into an exemption — the syntax
|
|
33
|
+
* authors already type (`'!**\/*.spec.ts'`) used to compile to a literal `!`
|
|
34
|
+
* glob that matched nothing, silently, while the spec file stayed governed.
|
|
35
|
+
*
|
|
36
|
+
* `from` is split by core's ONE `!` parser (`parseGlobList`), the same one
|
|
37
|
+
* every gate plane selects through. The planes differ in what a negation DOES,
|
|
38
|
+
* never in what it is: here it EXEMPTS (the file is still scanned, its
|
|
39
|
+
* violations are marked suppressed and counted — the boundary counterpart of
|
|
40
|
+
* policy `exemptFiles`); on the gate planes it EXCLUDES (out of scope). Both
|
|
41
|
+
* share one liveness rule: a negation is alive iff it removes at least one
|
|
42
|
+
* file from its own positive set. A bare `!` is rejected at load
|
|
43
|
+
* (`globListProblem`), so the empty-glob guard below only meets an
|
|
44
|
+
* unvalidated rule.
|
|
45
|
+
*/
|
|
46
|
+
export function boundaryRuleScope(rule) {
|
|
47
|
+
const { include, exclude } = parseGlobList(rule.from ?? []);
|
|
48
|
+
const exemptions = [];
|
|
49
|
+
for (const glob of exclude) {
|
|
50
|
+
if (glob.length > 0)
|
|
51
|
+
exemptions.push({ glob, origin: 'from-negation' });
|
|
52
|
+
}
|
|
53
|
+
for (const glob of rule.exemptFiles ?? [])
|
|
54
|
+
exemptions.push({ glob, origin: 'exemptFiles' });
|
|
55
|
+
if (rule.excludeTests === true) {
|
|
56
|
+
for (const glob of TEST_FILE_GLOBS)
|
|
57
|
+
exemptions.push({ glob, origin: 'excludeTests' });
|
|
58
|
+
}
|
|
59
|
+
return { include, exempt: exemptions.map((e) => e.glob), exemptions };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Where `relPath` sits relative to a precomputed scope: `in` (governed),
|
|
63
|
+
* `exempt` (inside `from`, but exempted — still scanned, its violations are
|
|
64
|
+
* MARKED suppressed, never dropped), or `out`.
|
|
65
|
+
*/
|
|
66
|
+
export function boundaryScopeDecision(scope, relPath) {
|
|
67
|
+
if (!matchesAny(relPath, scope.include))
|
|
68
|
+
return 'out';
|
|
69
|
+
if (scope.exempt.length > 0 && matchesAny(relPath, scope.exempt))
|
|
70
|
+
return 'exempt';
|
|
71
|
+
return 'in';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* {@link boundaryScopeDecision} for an UNREAD entry: a file the scan matched
|
|
75
|
+
* but could not read, or a directory it could not list. A file is governed
|
|
76
|
+
* exactly as a read one would be. A directory is governed when an include glob
|
|
77
|
+
* could match beneath it and no exemption covers ALL of it — through the one
|
|
78
|
+
* unread-entry test every plane uses (`unreadEntryMatches` /
|
|
79
|
+
* `unreadEntryWhollyMatches`).
|
|
80
|
+
*/
|
|
81
|
+
export function boundaryScopeCoversUnread(scope, u) {
|
|
82
|
+
if (!unreadEntryMatches(u, scope.include))
|
|
83
|
+
return false;
|
|
84
|
+
return !(scope.exempt.length > 0 && unreadEntryWhollyMatches(u, scope.exempt));
|
|
85
|
+
}
|
|
86
|
+
/** {@link boundaryScopeDecision} for one rule — the call every reader uses. */
|
|
87
|
+
export function boundaryRuleCovers(rule, relPath) {
|
|
88
|
+
return boundaryScopeDecision(boundaryRuleScope(rule), relPath);
|
|
89
|
+
}
|
|
90
|
+
/** The severity the evaluator ENFORCES: an unset severity is `error`. */
|
|
91
|
+
export function boundaryRuleSeverity(rule) {
|
|
92
|
+
return rule.severity ?? 'error';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether a rule whose scope matched no scanned file is a FAILURE rather than a
|
|
96
|
+
* skip — the gate planes' `failOnEmpty` default: on for `error` rules. The
|
|
97
|
+
* boundary VIEW of the one failOnEmpty authority (`failsWhenEmpty`,
|
|
98
|
+
* `@shrkcrft/core`), never a second default: an `info` rule, like a `warning`
|
|
99
|
+
* one, defaults off.
|
|
100
|
+
*/
|
|
101
|
+
export function boundaryRuleFailsOnEmpty(rule) {
|
|
102
|
+
return failsWhenEmpty({
|
|
103
|
+
...(rule.failOnEmpty !== undefined ? { failOnEmpty: rule.failOnEmpty } : {}),
|
|
104
|
+
severity: boundaryRuleSeverity(rule) === 'error' ? 'error' : 'warning',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** How the rule's `forbiddenImports` / `exceptions[].target` match: package semantics unless opted out. */
|
|
108
|
+
export function boundaryForbiddenMatch(rule) {
|
|
109
|
+
return rule.forbiddenMatch ?? 'package';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The patterns of one rule that can never change its verdict (round 12,
|
|
113
|
+
* R12-5.3 / R12-5.6) — the ONE answer the evaluator (coverage `subsumedBy` /
|
|
114
|
+
* `shadowedBy`, the shadowed dead unit), `boundaries explain` and MCP
|
|
115
|
+
* `get_boundary_rule` read, through the one subsumption proof beside the
|
|
116
|
+
* matcher (`importPatternSubsumes`).
|
|
117
|
+
*
|
|
118
|
+
* - `redundantForbidden`: a `forbiddenImports` entry another KEPT entry
|
|
119
|
+
* already covers — `@scope/pkg/**` next to `@scope/pkg` under package
|
|
120
|
+
* semantics, the `pkg` + `pkg/**` helper consumers wrote while a bare
|
|
121
|
+
* pattern missed subpaths. Of two entries covering each other the FIRST is
|
|
122
|
+
* kept, and a coverer is always a kept entry, so deleting every redundant
|
|
123
|
+
* one never narrows the fence. INFO only.
|
|
124
|
+
* - `shadowedAllowed`: an `allowedImports` entry a forbidden entry covers.
|
|
125
|
+
* Forbidden is checked first and allowed never re-admits, so it can never
|
|
126
|
+
* admit an import — under package semantics a bare forbidden package
|
|
127
|
+
* shadows every allowed subpath of it (a carve-out alpha.30 honoured).
|
|
128
|
+
*/
|
|
129
|
+
export function boundaryPatternOverlaps(rule) {
|
|
130
|
+
const mode = boundaryForbiddenMatch(rule);
|
|
131
|
+
const forbidden = rule.forbiddenImports ?? [];
|
|
132
|
+
const allowed = rule.allowedImports ?? [];
|
|
133
|
+
const covers = (outer, inner) => importPatternSubsumes(forbidden[outer], forbidden[inner], mode);
|
|
134
|
+
const coveredBy = new Map();
|
|
135
|
+
for (let i = 0; i < forbidden.length; i += 1) {
|
|
136
|
+
for (let j = 0; j < forbidden.length; j += 1) {
|
|
137
|
+
if (j === i || !covers(j, i))
|
|
138
|
+
continue;
|
|
139
|
+
if (j > i && covers(i, j))
|
|
140
|
+
continue; // mutual cover (e.g. a duplicate): the FIRST is kept
|
|
141
|
+
coveredBy.set(i, j);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// A coverer must itself be KEPT: were both ends of a chain flagged, deleting
|
|
146
|
+
// every redundant entry could drop the only one that enforced them.
|
|
147
|
+
for (let changed = true; changed;) {
|
|
148
|
+
changed = false;
|
|
149
|
+
for (const [i, j] of coveredBy) {
|
|
150
|
+
if (!coveredBy.has(j))
|
|
151
|
+
continue;
|
|
152
|
+
const kept = forbidden.findIndex((_, k) => k !== i && !coveredBy.has(k) && covers(k, i));
|
|
153
|
+
if (kept >= 0) {
|
|
154
|
+
coveredBy.set(i, kept);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
coveredBy.delete(i);
|
|
158
|
+
changed = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const redundantForbidden = [...coveredBy]
|
|
163
|
+
.sort(([a], [b]) => a - b)
|
|
164
|
+
.map(([index, by]) => ({ pattern: forbidden[index], index, by: forbidden[by] }));
|
|
165
|
+
const shadowedAllowed = [];
|
|
166
|
+
allowed.forEach((pattern, index) => {
|
|
167
|
+
const shadows = (k) => importPatternSubsumes(forbidden[k], pattern, mode, 'exact');
|
|
168
|
+
let by = forbidden.findIndex((_, k) => !coveredBy.has(k) && shadows(k));
|
|
169
|
+
if (by < 0)
|
|
170
|
+
by = forbidden.findIndex((_, k) => shadows(k));
|
|
171
|
+
if (by >= 0)
|
|
172
|
+
shadowedAllowed.push({ pattern, index, by: forbidden[by] });
|
|
173
|
+
});
|
|
174
|
+
return { redundantForbidden, shadowedAllowed };
|
|
175
|
+
}
|
|
@@ -1,32 +1,147 @@
|
|
|
1
|
+
import { type IAssetReference, type IUnitMark } from '@shrkcrft/core';
|
|
2
|
+
import type { IBoundaryRuleInput } from './boundary-rule-input.js';
|
|
1
3
|
export type BoundarySeverity = 'error' | 'warning' | 'info';
|
|
4
|
+
/**
|
|
5
|
+
* How a rule's `forbiddenImports` (and `exceptions[].target`) match a specifier
|
|
6
|
+
* (round 11, 1.5):
|
|
7
|
+
*
|
|
8
|
+
* - `package` (the default): a pattern with no `**` and no trailing `/` is a
|
|
9
|
+
* PACKAGE pattern — it matches the specifier and every subpath under it
|
|
10
|
+
* (`@scope/pkg` → `@scope/pkg/deep`, never `@scope/pkg-legacy`).
|
|
11
|
+
* - `exact`: the pattern is matched exactly as a glob — the entrypoint-only
|
|
12
|
+
* behaviour, for barrel-avoidance rules ("forbid `lodash`, allow
|
|
13
|
+
* `lodash/get`").
|
|
14
|
+
*
|
|
15
|
+
* `allowedImports` never takes package semantics: widening an allow-list is the
|
|
16
|
+
* permissive direction, which would silently turn existing red into green.
|
|
17
|
+
*/
|
|
18
|
+
export type ForbiddenMatchMode = 'package' | 'exact';
|
|
19
|
+
/** Which half of the pattern language matched a forbidden import. */
|
|
20
|
+
export type BoundaryMatchKind = 'exact' | 'subpath';
|
|
21
|
+
/** Where a file sits relative to a rule's scope: governed, exempted, or outside. */
|
|
22
|
+
export type BoundaryScopeDecision = 'in' | 'exempt' | 'out';
|
|
23
|
+
/** One exemption glob, and which rule field it came from. */
|
|
24
|
+
export interface IBoundaryScopeExemption {
|
|
25
|
+
readonly glob: string;
|
|
26
|
+
/** `from-negation` = a `!`-prefixed `from` entry; `excludeTests` = the shorthand. */
|
|
27
|
+
readonly origin: 'from-negation' | 'exemptFiles' | 'excludeTests';
|
|
28
|
+
}
|
|
29
|
+
/** A rule's normalised source-side scope. */
|
|
30
|
+
export interface IBoundaryRuleScope {
|
|
31
|
+
/** `from` globs that INCLUDE files (every entry not starting with `!`). */
|
|
32
|
+
readonly include: readonly string[];
|
|
33
|
+
/** Every exemption glob (from `!` entries, `exemptFiles`, `excludeTests`). */
|
|
34
|
+
readonly exempt: readonly string[];
|
|
35
|
+
readonly exemptions: readonly IBoundaryScopeExemption[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One adjudicated edge a rule allows. It is safe to ship only because it can
|
|
39
|
+
* rot loudly: an exception that no longer suppresses any real edge is a
|
|
40
|
+
* `stale-exception` ERROR, so the list can never grow into permanent silent
|
|
41
|
+
* width.
|
|
42
|
+
*/
|
|
43
|
+
export interface IBoundaryRuleException {
|
|
44
|
+
/** File glob of the importing file, matched like `from`. */
|
|
45
|
+
path: string;
|
|
46
|
+
/**
|
|
47
|
+
* Specifier pattern, matched with the rule's `forbiddenImports` semantics
|
|
48
|
+
* (alias candidates included). So under the default package semantics a
|
|
49
|
+
* bare target also excuses that package's SUBPATHS — `'@acme/sdk'` excuses
|
|
50
|
+
* `'@acme/sdk/client'` too: an exception allows one (path, target) PAIR,
|
|
51
|
+
* which may be more than one edge. Write the deepest subpath you mean;
|
|
52
|
+
* `forbiddenMatch: 'exact'` makes targets exact too. Deliberate — a target
|
|
53
|
+
* spelled like the forbidden pattern excuses exactly what that pattern flags
|
|
54
|
+
* (pinned by r76-exception-target-semantics).
|
|
55
|
+
*/
|
|
56
|
+
target: string;
|
|
57
|
+
/** Why the edge is sanctioned. Required — an unexplained exception is not an adjudication. */
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
2
60
|
export interface IBoundaryRule {
|
|
3
61
|
id: string;
|
|
4
62
|
title: string;
|
|
5
63
|
description?: string;
|
|
64
|
+
/** Unset means `error` — the evaluator enforces it so (see `boundaryRuleSeverity`). */
|
|
6
65
|
severity?: BoundarySeverity;
|
|
7
66
|
/**
|
|
8
67
|
* Glob patterns describing which files the rule applies to. Matched against
|
|
9
|
-
* the file path relative to the project root.
|
|
68
|
+
* the file path relative to the project root. An entry starting with `!` is
|
|
69
|
+
* an exemption (same as `exemptFiles`).
|
|
10
70
|
*/
|
|
11
71
|
from: readonly string[];
|
|
12
72
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
73
|
+
* Import specifiers forbidden from `from` files. Matched against the literal
|
|
74
|
+
* specifier AND its tsconfig-alias-resolved paths, with package semantics
|
|
75
|
+
* unless `forbiddenMatch: 'exact'` (see {@link ForbiddenMatchMode}).
|
|
15
76
|
*/
|
|
16
77
|
forbiddenImports?: readonly string[];
|
|
78
|
+
/** See {@link ForbiddenMatchMode}. Default `package`. */
|
|
79
|
+
forbiddenMatch?: ForbiddenMatchMode;
|
|
17
80
|
/**
|
|
18
81
|
* Optional whitelist of allowed imports (when set, non-matching imports
|
|
19
|
-
* also trigger the rule). Useful for "from X, only @x/y is allowed".
|
|
82
|
+
* also trigger the rule). Useful for "from X, only @x/y is allowed". Exact
|
|
83
|
+
* glob semantics — never widened to subpaths.
|
|
20
84
|
*/
|
|
21
85
|
allowedImports?: readonly string[];
|
|
86
|
+
/**
|
|
87
|
+
* Whether a rule whose `from` globs match NO scanned file fails the run (`1`)
|
|
88
|
+
* instead of being reported skipped (`2`). Default: `true` for `error`
|
|
89
|
+
* rules — the gate planes' contract. A rule matching nothing enforced
|
|
90
|
+
* nothing, whatever its neighbours did.
|
|
91
|
+
*/
|
|
92
|
+
failOnEmpty?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* File globs subtracted from the source side. Exempt files are still scanned;
|
|
95
|
+
* their violations are MARKED suppressed (`exempt-file`) and counted — never
|
|
96
|
+
* silently dropped. An exemption glob matching none of the rule's files is a
|
|
97
|
+
* dead unit.
|
|
98
|
+
*/
|
|
99
|
+
exemptFiles?: readonly string[];
|
|
100
|
+
/** Shorthand: exempt `**\/__tests__/**`, `**\/__mocks__/**`, `**\/*.spec.*`, `**\/*.test.*`. */
|
|
101
|
+
excludeTests?: boolean;
|
|
102
|
+
/** Adjudicated edges this rule allows — see {@link IBoundaryRuleException}. */
|
|
103
|
+
exceptions?: readonly IBoundaryRuleException[];
|
|
22
104
|
tags?: readonly string[];
|
|
23
105
|
appliesWhen?: readonly string[];
|
|
24
106
|
message?: string;
|
|
25
107
|
suggestedFix?: string;
|
|
26
108
|
relatedRules?: readonly string[];
|
|
27
109
|
relatedPathConventions?: readonly string[];
|
|
110
|
+
/**
|
|
111
|
+
* Verifiable pointers to what this rule is ABOUT (the directories and
|
|
112
|
+
* packages its globs describe) — the same shape knowledge entries declare,
|
|
113
|
+
* swept by the same `shrk knowledge stale-check`. An undeclared key used to be
|
|
114
|
+
* tolerated and checked by nothing, which is worse than absent: it looks
|
|
115
|
+
* policed.
|
|
116
|
+
*/
|
|
117
|
+
references?: readonly IAssetReference[];
|
|
118
|
+
/**
|
|
119
|
+
* The rule's `expectEmpty` markers (round 13) — DERIVED by the loader
|
|
120
|
+
* (`normalizeBoundaryRule`) from `{ pattern, expectEmpty: true, reason? }`
|
|
121
|
+
* entries in `from` / `exemptFiles` / `forbiddenImports` / `allowedImports`,
|
|
122
|
+
* each mark's `list` naming its list and its `packageName` stamped from the
|
|
123
|
+
* contributing pack. The lists above stay plain strings. Never authored: an
|
|
124
|
+
* authored rule is an {@link IBoundaryRuleInput}, and this key is refused on it.
|
|
125
|
+
*/
|
|
126
|
+
expectEmptyUnits?: readonly IUnitMark[];
|
|
127
|
+
}
|
|
128
|
+
/** Type a boundary rule as authored — the four selector lists accept `{ pattern, expectEmpty: true, reason? }` entries. */
|
|
129
|
+
export declare function defineBoundaryRule<T extends IBoundaryRuleInput>(rule: T): T;
|
|
130
|
+
/** One pattern of a rule that can never change its verdict, and the forbidden pattern that makes it so. */
|
|
131
|
+
export interface IBoundaryPatternOverlap {
|
|
132
|
+
readonly pattern: string;
|
|
133
|
+
/** Its position in its list (`forbiddenImports` or `allowedImports`). */
|
|
134
|
+
readonly index: number;
|
|
135
|
+
/** The forbidden pattern that already covers every import it matches. */
|
|
136
|
+
readonly by: string;
|
|
137
|
+
}
|
|
138
|
+
/** What `boundaryPatternOverlaps` proves about one rule's specifier lists (round 12, R12-5.3 / R12-5.6). */
|
|
139
|
+
export interface IBoundaryPatternOverlaps {
|
|
140
|
+
/** `forbiddenImports` entries a KEPT sibling already covers — deleting every one never narrows the fence. */
|
|
141
|
+
readonly redundantForbidden: readonly IBoundaryPatternOverlap[];
|
|
142
|
+
/** `allowedImports` entries a forbidden entry covers — forbidden is checked first, so they never admit an import. */
|
|
143
|
+
readonly shadowedAllowed: readonly IBoundaryPatternOverlap[];
|
|
28
144
|
}
|
|
29
|
-
export declare function defineBoundaryRule<T extends IBoundaryRule>(rule: T): T;
|
|
30
145
|
export interface IBoundaryRuleValidationIssue {
|
|
31
146
|
field: string;
|
|
32
147
|
message: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boundary-rule.d.ts","sourceRoot":"","sources":["../../src/model/boundary-rule.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B
|
|
1
|
+
{"version":3,"file":"boundary-rule.d.ts","sourceRoot":"","sources":["../../src/model/boundary-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,eAAe,EAEpB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAKnE,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAAC;AAErD,qEAAqE;AACrE,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,SAAS,CAAC;AAEpD,oFAAoF;AACpF,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE5D,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,GAAG,cAAc,CAAC;CACnE;AAED,6CAA6C;AAC7C,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACzD;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;;OAIG;IACH,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,yDAAyD;IACzD,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,gGAAgG;IAChG,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC/C,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACxC;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CACzC;AAED,2HAA2H;AAC3H,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,kBAAkB,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAE3E;AAED,2GAA2G;AAC3G,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,4GAA4G;AAC5G,MAAM,WAAW,wBAAwB;IACvC,6GAA6G;IAC7G,QAAQ,CAAC,kBAAkB,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAChE,qHAAqH;IACrH,QAAQ,CAAC,eAAe,EAAE,SAAS,uBAAuB,EAAE,CAAC;CAC9D;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AASD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IACpD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,4BAA4B,EAAE,CAAC;CACxC,CA6JA"}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { ASSET_REFERENCE_KINDS, exemptionListProblem, globListProblem, isMarkerObject, normalizeUnitList, unitProblemsOf, } from '@shrkcrft/core';
|
|
2
|
+
import { importPatternDefect } from "../scan/import-pattern.js";
|
|
3
|
+
import { BoundaryMarkableList } from "./boundary-markable-list.js";
|
|
4
|
+
import { boundaryRuleKeyProblems } from "./boundary-rule-key-problems.js";
|
|
5
|
+
import { boundaryRuleMarkerProblems } from "./boundary-rule-marker-problems.js";
|
|
6
|
+
import { boundaryUnitProblemIssue } from "./boundary-unit-problem-issue.js";
|
|
7
|
+
/** Type a boundary rule as authored — the four selector lists accept `{ pattern, expectEmpty: true, reason? }` entries. */
|
|
1
8
|
export function defineBoundaryRule(rule) {
|
|
2
9
|
return rule;
|
|
3
10
|
}
|
|
4
11
|
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
12
|
+
const SEVERITIES = ['error', 'warning', 'info'];
|
|
13
|
+
const FORBIDDEN_MATCH_MODES = ['package', 'exact'];
|
|
14
|
+
/** How a markable list may be written — named by every "must be a list" refusal. */
|
|
15
|
+
const LIST_ENTRY_SHAPE = 'strings, or { pattern, expectEmpty: true, reason? }';
|
|
5
16
|
export function validateBoundaryRule(value) {
|
|
6
17
|
const issues = [];
|
|
7
18
|
if (!value || typeof value !== 'object') {
|
|
@@ -11,14 +22,45 @@ export function validateBoundaryRule(value) {
|
|
|
11
22
|
};
|
|
12
23
|
}
|
|
13
24
|
const r = value;
|
|
25
|
+
// Round 13 — a key that is not a rule field (a rule-level `expectEmpty` /
|
|
26
|
+
// `allowDead` among them) used to load and be silently ignored.
|
|
27
|
+
issues.push(...boundaryRuleKeyProblems(r));
|
|
14
28
|
if (typeof r.id !== 'string' || !ID_PATTERN.test(r.id)) {
|
|
15
29
|
issues.push({ field: 'id', message: 'id required, slug-style' });
|
|
16
30
|
}
|
|
17
31
|
if (typeof r.title !== 'string' || r.title.length === 0) {
|
|
18
32
|
issues.push({ field: 'title', message: 'title required' });
|
|
19
33
|
}
|
|
34
|
+
// Round 13 — each markable list through core's ONE marker parser FIRST
|
|
35
|
+
// (every malformed marker named `<list>[i]: …`); every existing string check
|
|
36
|
+
// below then runs on the NORMALISED units, so `{ pattern: '!' }` is refused
|
|
37
|
+
// exactly like `'!'` and a marked pattern gets the same defect check.
|
|
38
|
+
const normalized = (field) => {
|
|
39
|
+
const raw = r[field];
|
|
40
|
+
if (!Array.isArray(raw))
|
|
41
|
+
return undefined;
|
|
42
|
+
const n = normalizeUnitList(raw, field);
|
|
43
|
+
if (!n.ok) {
|
|
44
|
+
for (const problem of unitProblemsOf(n.error))
|
|
45
|
+
issues.push(boundaryUnitProblemIssue(problem, field));
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return n.value;
|
|
49
|
+
};
|
|
50
|
+
const from = normalized(BoundaryMarkableList.From);
|
|
51
|
+
const forbidden = normalized(BoundaryMarkableList.ForbiddenImports);
|
|
52
|
+
const allowed = normalized(BoundaryMarkableList.AllowedImports);
|
|
53
|
+
const exempt = normalized(BoundaryMarkableList.ExemptFiles);
|
|
20
54
|
if (!Array.isArray(r.from) || r.from.length === 0) {
|
|
21
|
-
issues.push({ field: 'from', message:
|
|
55
|
+
issues.push({ field: 'from', message: `from must be a non-empty array of globs (${LIST_ENTRY_SHAPE})` });
|
|
56
|
+
}
|
|
57
|
+
else if (from !== undefined) {
|
|
58
|
+
// The one list-shape check, beside core's one `!` parser: a bare `!` (an
|
|
59
|
+
// empty exemption, silently dropped before round 12), a `!!x`, or only
|
|
60
|
+
// exemptions — each governs nothing, forever. On this plane `!` EXEMPTS.
|
|
61
|
+
const problem = globListProblem(from.units, 'are exemptions');
|
|
62
|
+
if (problem !== undefined)
|
|
63
|
+
issues.push({ field: 'from', message: `from ${problem}` });
|
|
22
64
|
}
|
|
23
65
|
if (!Array.isArray(r.forbiddenImports) && !Array.isArray(r.allowedImports)) {
|
|
24
66
|
issues.push({
|
|
@@ -26,5 +68,110 @@ export function validateBoundaryRule(value) {
|
|
|
26
68
|
message: 'either forbiddenImports or allowedImports must be set',
|
|
27
69
|
});
|
|
28
70
|
}
|
|
71
|
+
if (r.forbiddenImports !== undefined && !Array.isArray(r.forbiddenImports)) {
|
|
72
|
+
issues.push({ field: 'forbiddenImports', message: `forbiddenImports must be an array of import patterns (${LIST_ENTRY_SHAPE})` });
|
|
73
|
+
}
|
|
74
|
+
if (r.allowedImports !== undefined && !Array.isArray(r.allowedImports)) {
|
|
75
|
+
issues.push({ field: 'allowedImports', message: `allowedImports must be an array of import patterns (${LIST_ENTRY_SHAPE})` });
|
|
76
|
+
}
|
|
77
|
+
// R12-5.2 — a specifier pattern that cannot mean what it says (a `!`, an
|
|
78
|
+
// empty pattern, a trailing `/` under package semantics) is an authoring
|
|
79
|
+
// error: it used to load, match nothing its author meant, and let the gate
|
|
80
|
+
// print ✓ over imports of the very package it named. The one predicate
|
|
81
|
+
// (`importPatternDefect`) sits beside the matcher; `allowedImports` is always
|
|
82
|
+
// matched exactly, targets take the rule's mode. A normalised list keeps each
|
|
83
|
+
// entry at its index, so `[i]` names the authored entry.
|
|
84
|
+
const patternMode = r.forbiddenMatch === 'exact' ? 'exact' : 'package';
|
|
85
|
+
const checkPatterns = (field, list, mode) => {
|
|
86
|
+
list?.units.forEach((pattern, i) => {
|
|
87
|
+
const defect = importPatternDefect(pattern, mode);
|
|
88
|
+
if (defect !== undefined)
|
|
89
|
+
issues.push({ field: `${field}[${i}]`, message: `'${pattern}': ${defect}` });
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
checkPatterns(BoundaryMarkableList.ForbiddenImports, forbidden, patternMode);
|
|
93
|
+
checkPatterns(BoundaryMarkableList.AllowedImports, allowed, 'exact');
|
|
94
|
+
// A marker on a pattern dead by its shape, and failOnEmpty: true over an
|
|
95
|
+
// all-marked `from` — refused (DECISIONS §4).
|
|
96
|
+
issues.push(...boundaryRuleMarkerProblems({ from, forbiddenImports: forbidden, allowedImports: allowed }, patternMode, r.failOnEmpty));
|
|
97
|
+
if (r.severity !== undefined && (typeof r.severity !== 'string' || !SEVERITIES.includes(r.severity))) {
|
|
98
|
+
issues.push({ field: 'severity', message: `severity must be one of ${SEVERITIES.join(', ')}` });
|
|
99
|
+
}
|
|
100
|
+
if (r.forbiddenMatch !== undefined &&
|
|
101
|
+
(typeof r.forbiddenMatch !== 'string' || !FORBIDDEN_MATCH_MODES.includes(r.forbiddenMatch))) {
|
|
102
|
+
issues.push({
|
|
103
|
+
field: 'forbiddenMatch',
|
|
104
|
+
message: `forbiddenMatch must be one of ${FORBIDDEN_MATCH_MODES.join(', ')}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (r.failOnEmpty !== undefined && typeof r.failOnEmpty !== 'boolean') {
|
|
108
|
+
issues.push({ field: 'failOnEmpty', message: 'failOnEmpty must be a boolean' });
|
|
109
|
+
}
|
|
110
|
+
if (r.excludeTests !== undefined && typeof r.excludeTests !== 'boolean') {
|
|
111
|
+
issues.push({ field: 'excludeTests', message: 'excludeTests must be a boolean' });
|
|
112
|
+
}
|
|
113
|
+
if (r.exemptFiles !== undefined && !Array.isArray(r.exemptFiles)) {
|
|
114
|
+
issues.push({ field: 'exemptFiles', message: `exemptFiles must be an array of globs (${LIST_ENTRY_SHAPE})` });
|
|
115
|
+
}
|
|
116
|
+
else if (exempt !== undefined) {
|
|
117
|
+
const problem = exemptionListProblem(exempt.units);
|
|
118
|
+
if (problem !== undefined)
|
|
119
|
+
issues.push({ field: 'exemptFiles', message: `exemptFiles ${problem}` });
|
|
120
|
+
}
|
|
121
|
+
if (r.exceptions !== undefined) {
|
|
122
|
+
if (!Array.isArray(r.exceptions)) {
|
|
123
|
+
issues.push({ field: 'exceptions', message: 'exceptions must be an array of { path, target, reason }' });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
r.exceptions.forEach((e, i) => {
|
|
127
|
+
const ex = (e ?? {});
|
|
128
|
+
for (const key of ['path', 'target', 'reason']) {
|
|
129
|
+
const v = ex[key];
|
|
130
|
+
if (key === 'target' && isMarkerObject(v)) {
|
|
131
|
+
// Round 13: an exception adjudicates a real edge; one written
|
|
132
|
+
// ahead of its edge is width with nothing to adjudicate, and it
|
|
133
|
+
// must rot loudly (stale-exception, exit 1) — never be waived.
|
|
134
|
+
issues.push({
|
|
135
|
+
field: `exceptions[${i}].target`,
|
|
136
|
+
message: 'exceptions take no expectEmpty — a stale exception is an error by design',
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (typeof v !== 'string' || v.trim().length === 0) {
|
|
141
|
+
issues.push({
|
|
142
|
+
field: `exceptions[${i}].${key}`,
|
|
143
|
+
message: key === 'reason'
|
|
144
|
+
? 'an exception needs a non-empty reason — an unexplained exception is not an adjudication'
|
|
145
|
+
: `${key} must be a non-empty string`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (typeof ex.target === 'string' && ex.target.trim().length > 0) {
|
|
150
|
+
const defect = importPatternDefect(ex.target, patternMode);
|
|
151
|
+
if (defect !== undefined) {
|
|
152
|
+
issues.push({ field: `exceptions[${i}].target`, message: `'${ex.target}': ${defect}` });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (r.references !== undefined) {
|
|
159
|
+
// Shape only — whether each target still resolves is the staleness
|
|
160
|
+
// sweep's job (`shrk knowledge stale-check`), against the live tree.
|
|
161
|
+
if (!Array.isArray(r.references)) {
|
|
162
|
+
issues.push({ field: 'references', message: 'references must be an array of { kind, path | id | symbol }' });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
r.references.forEach((ref, i) => {
|
|
166
|
+
const kind = ref?.kind;
|
|
167
|
+
if (typeof kind !== 'string' || !ASSET_REFERENCE_KINDS.includes(kind)) {
|
|
168
|
+
issues.push({
|
|
169
|
+
field: `references[${i}].kind`,
|
|
170
|
+
message: `kind must be one of ${ASSET_REFERENCE_KINDS.join(', ')}`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
29
176
|
return { valid: issues.length === 0, issues };
|
|
30
177
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IBoundaryRuleValidationIssue } from './boundary-rule.js';
|
|
2
|
+
/**
|
|
3
|
+
* One core marker problem (`<listPath>[i]: …`, from `normalizeUnitList`) as a
|
|
4
|
+
* boundary validation issue — field `<listPath>[i]`, message the sentence after
|
|
5
|
+
* it — so every surface prints `forbiddenImports[1]: …` once, in the shape
|
|
6
|
+
* every other boundary issue has (`field: message`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function boundaryUnitProblemIssue(problem: string, listPath: string): IBoundaryRuleValidationIssue;
|
|
9
|
+
//# sourceMappingURL=boundary-unit-problem-issue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary-unit-problem-issue.d.ts","sourceRoot":"","sources":["../../src/model/boundary-unit-problem-issue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAEvE;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,4BAA4B,CAOxG"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One core marker problem (`<listPath>[i]: …`, from `normalizeUnitList`) as a
|
|
3
|
+
* boundary validation issue — field `<listPath>[i]`, message the sentence after
|
|
4
|
+
* it — so every surface prints `forbiddenImports[1]: …` once, in the shape
|
|
5
|
+
* every other boundary issue has (`field: message`).
|
|
6
|
+
*/
|
|
7
|
+
export function boundaryUnitProblemIssue(problem, listPath) {
|
|
8
|
+
if (problem.startsWith(`${listPath}[`)) {
|
|
9
|
+
const end = problem.indexOf(']: ');
|
|
10
|
+
if (end > 0)
|
|
11
|
+
return { field: problem.slice(0, end + 1), message: problem.slice(end + 3) };
|
|
12
|
+
}
|
|
13
|
+
if (problem.startsWith(`${listPath}: `))
|
|
14
|
+
return { field: listPath, message: problem.slice(listPath.length + 2) };
|
|
15
|
+
return { field: listPath, message: problem };
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AppErrorImpl, type Result } from '@shrkcrft/core';
|
|
2
|
+
import type { IBoundaryRule } from './boundary-rule.js';
|
|
3
|
+
import type { IBoundaryRuleInput } from './boundary-rule-input.js';
|
|
4
|
+
/**
|
|
5
|
+
* An authored boundary rule as the LOADED rule (round 13): each of the four
|
|
6
|
+
* selector lists through core's ONE marker parser (`normalizeUnitList`) — the
|
|
7
|
+
* plain string list every reader already consumes, and every
|
|
8
|
+
* `{ pattern, expectEmpty: true, reason? }` entry as a mark in ONE
|
|
9
|
+
* `expectEmptyUnits` ledger, stamped with the contributing pack
|
|
10
|
+
* (`stampUnitMarks`) when the loader knows it.
|
|
11
|
+
*
|
|
12
|
+
* Called by the loader after validation (local rules, pack `boundaryFiles`,
|
|
13
|
+
* `--rule-file` / `--diff-against`) and, IDEMPOTENTLY, at the top of
|
|
14
|
+
* `evaluateBoundaries` — so a hand-built rule (the plan-review /
|
|
15
|
+
* plan-simulation call sites, any API caller) never crashes on an object
|
|
16
|
+
* pattern and never silently ignores a marker. A rule with nothing to
|
|
17
|
+
* normalise comes back as the same object; a malformed entry is an error
|
|
18
|
+
* carrying every `<list>[i]: …` problem (`details.problems`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeBoundaryRule(rule: IBoundaryRuleInput | IBoundaryRule, packageName?: string): Result<IBoundaryRule, AppErrorImpl>;
|
|
21
|
+
//# sourceMappingURL=normalize-boundary-rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-boundary-rule.d.ts","sourceRoot":"","sources":["../../src/model/normalize-boundary-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EASZ,KAAK,MAAM,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAKnE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,kBAAkB,GAAG,aAAa,EACxC,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAAC,aAAa,EAAE,YAAY,CAAC,CA0CrC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AppErrorImpl, ERROR_CODES, err, mergeUnitMarks, normalizeUnitList, ok, stampUnitMarks, unitProblemsOf, } from '@shrkcrft/core';
|
|
2
|
+
import { BoundaryMarkableList } from "./boundary-markable-list.js";
|
|
3
|
+
const LISTS = Object.values(BoundaryMarkableList);
|
|
4
|
+
/**
|
|
5
|
+
* An authored boundary rule as the LOADED rule (round 13): each of the four
|
|
6
|
+
* selector lists through core's ONE marker parser (`normalizeUnitList`) — the
|
|
7
|
+
* plain string list every reader already consumes, and every
|
|
8
|
+
* `{ pattern, expectEmpty: true, reason? }` entry as a mark in ONE
|
|
9
|
+
* `expectEmptyUnits` ledger, stamped with the contributing pack
|
|
10
|
+
* (`stampUnitMarks`) when the loader knows it.
|
|
11
|
+
*
|
|
12
|
+
* Called by the loader after validation (local rules, pack `boundaryFiles`,
|
|
13
|
+
* `--rule-file` / `--diff-against`) and, IDEMPOTENTLY, at the top of
|
|
14
|
+
* `evaluateBoundaries` — so a hand-built rule (the plan-review /
|
|
15
|
+
* plan-simulation call sites, any API caller) never crashes on an object
|
|
16
|
+
* pattern and never silently ignores a marker. A rule with nothing to
|
|
17
|
+
* normalise comes back as the same object; a malformed entry is an error
|
|
18
|
+
* carrying every `<list>[i]: …` problem (`details.problems`).
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeBoundaryRule(rule, packageName) {
|
|
21
|
+
const raw = rule;
|
|
22
|
+
const problems = [];
|
|
23
|
+
const normalized = new Map();
|
|
24
|
+
for (const list of LISTS) {
|
|
25
|
+
const entries = raw[list];
|
|
26
|
+
if (entries === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
const n = normalizeUnitList(entries, list);
|
|
29
|
+
if (!n.ok) {
|
|
30
|
+
problems.push(...unitProblemsOf(n.error));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
normalized.set(list, n.value);
|
|
34
|
+
}
|
|
35
|
+
if (problems.length > 0) {
|
|
36
|
+
const id = typeof raw['id'] === 'string' ? raw['id'] : '(no id)';
|
|
37
|
+
const more = problems.length > 1 ? ` (+${problems.length - 1} more)` : '';
|
|
38
|
+
return err(new AppErrorImpl(ERROR_CODES.CONFIG_INVALID, `boundary rule '${id}': ${problems[0]}${more}`, {
|
|
39
|
+
details: { ruleId: id, problems: [...problems] },
|
|
40
|
+
suggestion: 'write each entry as a plain string, or as { pattern, expectEmpty: true, reason? }',
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
const loaded = rule;
|
|
44
|
+
const found = [...normalized.values()].flatMap((l) => l.marks);
|
|
45
|
+
const existing = loaded.expectEmptyUnits ?? [];
|
|
46
|
+
const stamps = packageName !== undefined && packageName.length > 0 && existing.length > 0;
|
|
47
|
+
if (found.length === 0 && !stamps)
|
|
48
|
+
return ok(loaded);
|
|
49
|
+
const marks = stampUnitMarks(mergeUnitMarks(existing, found), packageName);
|
|
50
|
+
const units = (list) => normalized.get(list)?.units;
|
|
51
|
+
const exemptFiles = units(BoundaryMarkableList.ExemptFiles);
|
|
52
|
+
const forbiddenImports = units(BoundaryMarkableList.ForbiddenImports);
|
|
53
|
+
const allowedImports = units(BoundaryMarkableList.AllowedImports);
|
|
54
|
+
return ok({
|
|
55
|
+
...loaded,
|
|
56
|
+
from: units(BoundaryMarkableList.From) ?? loaded.from,
|
|
57
|
+
...(exemptFiles !== undefined ? { exemptFiles } : {}),
|
|
58
|
+
...(forbiddenImports !== undefined ? { forbiddenImports } : {}),
|
|
59
|
+
...(allowedImports !== undefined ? { allowedImports } : {}),
|
|
60
|
+
...(marks.length > 0 ? { expectEmptyUnits: marks } : {}),
|
|
61
|
+
});
|
|
62
|
+
}
|