@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.
Files changed (226) hide show
  1. package/dist/baseline/compute-baseline.d.ts +8 -0
  2. package/dist/baseline/compute-baseline.d.ts.map +1 -1
  3. package/dist/baseline/compute-baseline.js +8 -7
  4. package/dist/baseline/diff-baseline.d.ts +28 -0
  5. package/dist/baseline/diff-baseline.d.ts.map +1 -1
  6. package/dist/baseline/diff-baseline.js +29 -0
  7. package/dist/evaluate/boundary-unit-finding.d.ts +10 -0
  8. package/dist/evaluate/boundary-unit-finding.d.ts.map +1 -0
  9. package/dist/evaluate/boundary-unit-finding.js +21 -0
  10. package/dist/evaluate/boundary-unit-kind.d.ts +7 -0
  11. package/dist/evaluate/boundary-unit-kind.d.ts.map +1 -0
  12. package/dist/evaluate/boundary-unit-kind.js +14 -0
  13. package/dist/evaluate/evaluate-boundaries.d.ts +230 -3
  14. package/dist/evaluate/evaluate-boundaries.d.ts.map +1 -1
  15. package/dist/evaluate/evaluate-boundaries.js +481 -53
  16. package/dist/evaluate/i-boundary-rule-settle-input.d.ts +27 -0
  17. package/dist/evaluate/i-boundary-rule-settle-input.d.ts.map +1 -0
  18. package/dist/evaluate/i-boundary-rule-settle-input.js +1 -0
  19. package/dist/evaluate/i-boundary-rule-settlement.d.ts +23 -0
  20. package/dist/evaluate/i-boundary-rule-settlement.d.ts.map +1 -0
  21. package/dist/evaluate/i-boundary-rule-settlement.js +1 -0
  22. package/dist/evaluate/i-boundary-unit-finding.d.ts +22 -0
  23. package/dist/evaluate/i-boundary-unit-finding.d.ts.map +1 -0
  24. package/dist/evaluate/i-boundary-unit-finding.js +1 -0
  25. package/dist/evaluate/settle-boundary-rule.d.ts +23 -0
  26. package/dist/evaluate/settle-boundary-rule.d.ts.map +1 -0
  27. package/dist/evaluate/settle-boundary-rule.js +73 -0
  28. package/dist/evaluate/with-boundary-rule-settlement.d.ts +11 -0
  29. package/dist/evaluate/with-boundary-rule-settlement.d.ts.map +1 -0
  30. package/dist/evaluate/with-boundary-rule-settlement.js +44 -0
  31. package/dist/extract/code-zones.d.ts +95 -3
  32. package/dist/extract/code-zones.d.ts.map +1 -1
  33. package/dist/extract/code-zones.js +313 -4
  34. package/dist/extract/extract-tokens.d.ts +30 -0
  35. package/dist/extract/extract-tokens.d.ts.map +1 -1
  36. package/dist/extract/extract-tokens.js +233 -27
  37. package/dist/extract/i-labeled-source.d.ts +12 -0
  38. package/dist/extract/i-labeled-source.d.ts.map +1 -0
  39. package/dist/extract/i-labeled-source.js +1 -0
  40. package/dist/extract/import-edges.d.ts +1 -0
  41. package/dist/extract/import-edges.d.ts.map +1 -1
  42. package/dist/extract/import-edges.js +20 -7
  43. package/dist/extract/inspect-source.d.ts +32 -3
  44. package/dist/extract/inspect-source.d.ts.map +1 -1
  45. package/dist/extract/inspect-source.js +50 -7
  46. package/dist/extract/parse-imports.d.ts +34 -12
  47. package/dist/extract/parse-imports.d.ts.map +1 -1
  48. package/dist/extract/parse-imports.js +119 -20
  49. package/dist/extract/scan-literals.d.ts +12 -1
  50. package/dist/extract/scan-literals.d.ts.map +1 -1
  51. package/dist/extract/scan-literals.js +15 -1
  52. package/dist/extract/source-liveness-request.d.ts +24 -0
  53. package/dist/extract/source-liveness-request.d.ts.map +1 -0
  54. package/dist/extract/source-liveness-request.js +45 -0
  55. package/dist/generated/check-provenance.d.ts.map +1 -1
  56. package/dist/generated/check-provenance.js +3 -1
  57. package/dist/generated/read-regen-tree.d.ts +16 -0
  58. package/dist/generated/read-regen-tree.d.ts.map +1 -0
  59. package/dist/generated/read-regen-tree.js +61 -0
  60. package/dist/generated/scan-generated.d.ts +8 -0
  61. package/dist/generated/scan-generated.d.ts.map +1 -1
  62. package/dist/generated/scan-generated.js +25 -10
  63. package/dist/index.d.ts +48 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +51 -0
  66. package/dist/model/boundary-intended-empty-line.d.ts +10 -0
  67. package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
  68. package/dist/model/boundary-intended-empty-line.js +11 -0
  69. package/dist/model/boundary-markable-list.d.ts +15 -0
  70. package/dist/model/boundary-markable-list.d.ts.map +1 -0
  71. package/dist/model/boundary-markable-list.js +15 -0
  72. package/dist/model/boundary-rule-input-keys.d.ts +11 -0
  73. package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
  74. package/dist/model/boundary-rule-input-keys.js +29 -0
  75. package/dist/model/boundary-rule-input.d.ts +23 -0
  76. package/dist/model/boundary-rule-input.d.ts.map +1 -0
  77. package/dist/model/boundary-rule-input.js +1 -0
  78. package/dist/model/boundary-rule-key-problems.d.ts +11 -0
  79. package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
  80. package/dist/model/boundary-rule-key-problems.js +36 -0
  81. package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
  82. package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
  83. package/dist/model/boundary-rule-marker-problems.js +94 -0
  84. package/dist/model/boundary-rule-scope.d.ts +88 -0
  85. package/dist/model/boundary-rule-scope.d.ts.map +1 -0
  86. package/dist/model/boundary-rule-scope.js +175 -0
  87. package/dist/model/boundary-rule.d.ts +120 -5
  88. package/dist/model/boundary-rule.d.ts.map +1 -1
  89. package/dist/model/boundary-rule.js +148 -1
  90. package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
  91. package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
  92. package/dist/model/boundary-unit-problem-issue.js +16 -0
  93. package/dist/model/normalize-boundary-rule.d.ts +21 -0
  94. package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
  95. package/dist/model/normalize-boundary-rule.js +62 -0
  96. package/dist/policy/evaluate-policy.d.ts +80 -3
  97. package/dist/policy/evaluate-policy.d.ts.map +1 -1
  98. package/dist/policy/evaluate-policy.js +152 -19
  99. package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
  100. package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
  101. package/dist/policy/i-policy-rule-liveness.js +1 -0
  102. package/dist/policy/run-policy.d.ts +1 -1
  103. package/dist/policy/run-policy.d.ts.map +1 -1
  104. package/dist/policy/run-policy.js +171 -21
  105. package/dist/registry/load-boundary-rules.d.ts +29 -1
  106. package/dist/registry/load-boundary-rules.d.ts.map +1 -1
  107. package/dist/registry/load-boundary-rules.js +38 -8
  108. package/dist/scan/glob.d.ts +90 -0
  109. package/dist/scan/glob.d.ts.map +1 -1
  110. package/dist/scan/glob.js +205 -0
  111. package/dist/scan/i-glob-unit-measure.d.ts +22 -0
  112. package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
  113. package/dist/scan/i-glob-unit-measure.js +1 -0
  114. package/dist/scan/import-pattern.d.ts +109 -0
  115. package/dist/scan/import-pattern.d.ts.map +1 -0
  116. package/dist/scan/import-pattern.js +191 -0
  117. package/dist/scan/node-builtin-package-names.d.ts +13 -0
  118. package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
  119. package/dist/scan/node-builtin-package-names.js +25 -0
  120. package/dist/scan/scan-imports.d.ts +36 -3
  121. package/dist/scan/scan-imports.d.ts.map +1 -1
  122. package/dist/scan/scan-imports.js +114 -45
  123. package/dist/util/blank-run-hazard-finding.d.ts +34 -0
  124. package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
  125. package/dist/util/blank-run-hazard-finding.js +1 -0
  126. package/dist/util/blank-run-hazard.d.ts +29 -0
  127. package/dist/util/blank-run-hazard.d.ts.map +1 -0
  128. package/dist/util/blank-run-hazard.js +450 -0
  129. package/dist/util/dead-glob-units.d.ts +46 -0
  130. package/dist/util/dead-glob-units.d.ts.map +1 -0
  131. package/dist/util/dead-glob-units.js +116 -0
  132. package/dist/util/glob-list-liveness-input.d.ts +31 -0
  133. package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
  134. package/dist/util/glob-list-liveness-input.js +65 -0
  135. package/dist/util/i-dead-glob-unit.d.ts +24 -0
  136. package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
  137. package/dist/util/i-dead-glob-unit.js +1 -0
  138. package/dist/util/i-glob-list-units.d.ts +25 -0
  139. package/dist/util/i-glob-list-units.d.ts.map +1 -0
  140. package/dist/util/i-glob-list-units.js +1 -0
  141. package/dist/util/i-glob-liveness-list.d.ts +23 -0
  142. package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
  143. package/dist/util/i-glob-liveness-list.js +1 -0
  144. package/dist/util/i-glob-liveness-request.d.ts +11 -0
  145. package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
  146. package/dist/util/i-glob-liveness-request.js +1 -0
  147. package/dist/util/i-glob-negation.d.ts +13 -0
  148. package/dist/util/i-glob-negation.d.ts.map +1 -0
  149. package/dist/util/i-glob-negation.js +1 -0
  150. package/dist/util/matched-files.d.ts +22 -0
  151. package/dist/util/matched-files.d.ts.map +1 -0
  152. package/dist/util/matched-files.js +1 -0
  153. package/dist/util/negation-cause.d.ts +15 -0
  154. package/dist/util/negation-cause.d.ts.map +1 -0
  155. package/dist/util/negation-cause.js +21 -0
  156. package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
  157. package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
  158. package/dist/util/plane-scan-exclude-dirs.js +22 -0
  159. package/dist/util/read-glob-list-liveness.d.ts +20 -0
  160. package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
  161. package/dist/util/read-glob-list-liveness.js +20 -0
  162. package/dist/util/read-scope-coverage.d.ts +90 -0
  163. package/dist/util/read-scope-coverage.d.ts.map +1 -0
  164. package/dist/util/read-scope-coverage.js +173 -0
  165. package/dist/util/read-scope.d.ts +14 -0
  166. package/dist/util/read-scope.d.ts.map +1 -0
  167. package/dist/util/read-scope.js +1 -0
  168. package/dist/util/read-selected-files.d.ts +16 -0
  169. package/dist/util/read-selected-files.d.ts.map +1 -0
  170. package/dist/util/read-selected-files.js +25 -0
  171. package/dist/util/settle-glob-lists.d.ts +12 -0
  172. package/dist/util/settle-glob-lists.d.ts.map +1 -0
  173. package/dist/util/settle-glob-lists.js +13 -0
  174. package/dist/util/unread-file-reason.d.ts +26 -0
  175. package/dist/util/unread-file-reason.d.ts.map +1 -0
  176. package/dist/util/unread-file-reason.js +26 -0
  177. package/dist/util/unread-file.d.ts +10 -0
  178. package/dist/util/unread-file.d.ts.map +1 -0
  179. package/dist/util/unread-file.js +1 -0
  180. package/dist/util/walk-files.d.ts +74 -10
  181. package/dist/util/walk-files.d.ts.map +1 -1
  182. package/dist/util/walk-files.js +137 -31
  183. package/dist/wiring/evaluate-wiring.d.ts +79 -5
  184. package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
  185. package/dist/wiring/evaluate-wiring.js +267 -12
  186. package/dist/wiring/explain-wiring.d.ts +49 -5
  187. package/dist/wiring/explain-wiring.d.ts.map +1 -1
  188. package/dist/wiring/explain-wiring.js +118 -10
  189. package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
  190. package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
  191. package/dist/wiring/i-idiom-role-coverage.js +1 -0
  192. package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
  193. package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
  194. package/dist/wiring/i-registration-query-verdict.js +1 -0
  195. package/dist/wiring/i-registration-roles.d.ts +71 -0
  196. package/dist/wiring/i-registration-roles.d.ts.map +1 -0
  197. package/dist/wiring/i-registration-roles.js +1 -0
  198. package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
  199. package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
  200. package/dist/wiring/measure-idiom-role-coverage.js +20 -0
  201. package/dist/wiring/measure-registration-roles.d.ts +25 -0
  202. package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
  203. package/dist/wiring/measure-registration-roles.js +124 -0
  204. package/dist/wiring/plan-wiring-fix.js +2 -2
  205. package/dist/wiring/registration-graph.d.ts +12 -0
  206. package/dist/wiring/registration-graph.d.ts.map +1 -1
  207. package/dist/wiring/registration-graph.js +16 -5
  208. package/dist/wiring/registration-query-verdict.d.ts +23 -0
  209. package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
  210. package/dist/wiring/registration-query-verdict.js +118 -0
  211. package/dist/wiring/registry-query.d.ts +8 -0
  212. package/dist/wiring/registry-query.d.ts.map +1 -1
  213. package/dist/wiring/registry-query.js +12 -5
  214. package/dist/wiring/scan-wiring-files.d.ts +4 -2
  215. package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
  216. package/dist/wiring/scan-wiring-files.js +52 -15
  217. package/dist/wiring/sink-imports.d.ts +11 -0
  218. package/dist/wiring/sink-imports.d.ts.map +1 -1
  219. package/dist/wiring/sink-imports.js +11 -2
  220. package/dist/wiring/trace-literal.d.ts +6 -0
  221. package/dist/wiring/trace-literal.d.ts.map +1 -1
  222. package/dist/wiring/trace-literal.js +5 -2
  223. package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
  224. package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
  225. package/dist/wiring/wiring-labeled-sources.js +20 -0
  226. 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
- * Glob patterns describing imports that are forbidden from `from` files.
14
- * Matched against the literal import specifier.
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;;;OAGG;IACH,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;OAGG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,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;CAC5C;AAED,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,aAAa,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAEtE;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IACpD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,4BAA4B,EAAE,CAAC;CACxC,CAyBA"}
1
+ {"version":3,"file":"boundary-rule.d.ts","sourceRoot":"","sources":["../../src/model/boundary-rule.ts"],"names":[],"mappings":"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"}
@@ -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: 'from must be a non-empty string array' });
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
+ }