@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,191 @@
1
+ import { globToRegex } from "./glob.js";
2
+ import { nodeBuiltinPackageNames } from "./node-builtin-package-names.js";
3
+ /**
4
+ * How a forbidden-import (or exception-target) pattern matches an import
5
+ * specifier — the pattern language of the boundary plane (round 11, 1.5).
6
+ *
7
+ * The generic glob matcher keeps `*` = "any chars except `/`" (glob.test.ts
8
+ * pins that), so a bare package pattern like `@scope/package-a` or
9
+ * `@scope/package-*` used to match ONLY the package entrypoint: every subpath
10
+ * import of the very same forbidden package (`@scope/package-a/sub`) escaped
11
+ * the fence, and the gate reported green over it. Package semantics live HERE,
12
+ * in the boundary evaluator's matcher, not in glob.ts.
13
+ *
14
+ * The rule, in one sentence: a pattern with no `**` and no trailing `/` is a
15
+ * PACKAGE pattern — it matches the specifier itself and everything under it
16
+ * (`<pattern>/**`), at a segment boundary, so `@scope/pkg` covers
17
+ * `@scope/pkg/deep` but never `@scope/pkg-legacy`. A pattern containing `**`
18
+ * already says how deep it reaches and is matched exactly as written. A rule
19
+ * that means "the entrypoint only" (barrel avoidance: forbid `lodash`, allow
20
+ * `lodash/get`) opts out with `forbiddenMatch: 'exact'`.
21
+ */
22
+ /** Does `pattern` take package semantics (entrypoint + every subpath)? */
23
+ export function isPackagePattern(pattern) {
24
+ return !pattern.includes('**') && !pattern.endsWith('/');
25
+ }
26
+ /**
27
+ * Match `specifier` against `pattern`.
28
+ *
29
+ * Returns `'exact'` when the pattern matches the specifier as written,
30
+ * `'subpath'` when only the package semantics reached it (a deeper import of a
31
+ * package the pattern names), and `null` otherwise. `mode: 'exact'` disables
32
+ * the subpath half. The verdict label travels on the violation (`matchKind`)
33
+ * so a newly-reported subpath edge explains itself.
34
+ */
35
+ export function matchImportPattern(specifier, pattern, mode = 'package') {
36
+ if (globToRegex(pattern).test(specifier))
37
+ return 'exact';
38
+ if (mode === 'package' && isPackagePattern(pattern) && globToRegex(`${pattern}/**`).test(specifier)) {
39
+ return 'subpath';
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * Why `pattern` cannot mean what its author wrote in a SPECIFIER list
45
+ * (`forbiddenImports`, `allowedImports`, `exceptions[].target`) matched under
46
+ * `mode` — or `undefined` for a well-formed pattern (round 12, R12-5.2). The
47
+ * one predicate the rule validator (every local AND pack rule file) and the
48
+ * evaluator's dead-unit reach both read.
49
+ *
50
+ * - `''` names no import.
51
+ * - A leading `!` is the NEGATION syntax of every glob list in this repo, but
52
+ * a specifier list takes none: compiled as written it matches only an
53
+ * import that itself starts with `!` (a webpack inline loader), so the
54
+ * carve-out an author meant silently matched nothing. (A literal
55
+ * inline-loader specifier is still reachable: start the pattern with `?`.)
56
+ * - Under package semantics a trailing `/` is the one spelling left literal
57
+ * ({@link isPackagePattern}): it matches only an import written WITH that
58
+ * slash (`'buffer/'`, the userland-polyfill idiom) — never the package, never
59
+ * its subpaths — while the gate reported ✓ over both. `mode: 'exact'` (and
60
+ * `allowedImports`, always exact) keeps it as the literal it then plainly is.
61
+ *
62
+ * Deliberately NOT "can never match": `'buffer/'` is a real import specifier.
63
+ * The trailing-slash fix it names is lossless — the package spelling covers
64
+ * the slash spelling too (`matchImportPattern('buffer/', 'buffer')` is
65
+ * `subpath`), so following the advice never narrows a fence.
66
+ */
67
+ export function importPatternDefect(pattern, mode = 'package') {
68
+ if (pattern.length === 0)
69
+ return 'an empty pattern names no import';
70
+ if (pattern.startsWith('!')) {
71
+ return ('negation is only supported in `from` — a specifier list takes no "!" (as written this matches only an import that itself starts with "!"). ' +
72
+ 'forbiddenImports is checked before allowedImports, so allowedImports never re-admits a forbidden import: carve a subpath out with exceptions[{ path, target, reason }], ' +
73
+ "or set forbiddenMatch: 'exact' and list exactly what you forbid");
74
+ }
75
+ if (mode === 'package' && pattern.endsWith('/')) {
76
+ const bare = pattern.replace(/\/+$/, '');
77
+ const fix = bare.length === 0
78
+ ? 'name a package or a path'
79
+ : isPackagePattern(bare)
80
+ ? `write '${bare}' (the package and every subpath — '${pattern}' included) or '${bare}/**' (subpaths only)`
81
+ : `write '${bare}'`;
82
+ return `a trailing '/' matches only an import written with that slash, never the package or its subpaths — ${fix}; forbiddenMatch: 'exact' keeps '${pattern}' as a literal`;
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Does every specifier `inner` matches also match `outer`? (round 12, R12-5.3 /
88
+ * R12-5.6). `outerMode` is how `outer` matches (the rule's `forbiddenMatch`);
89
+ * `innerMode` is how `inner` does — the same mode for a sibling forbidden
90
+ * pattern, `exact` for an `allowedImports` entry (never widened).
91
+ *
92
+ * Deliberately CONSERVATIVE — `true` only when the literal shape proves it,
93
+ * because a false "covered" would tell an author to delete a live pattern (or
94
+ * call a working allowance dead). Two proofs:
95
+ *
96
+ * 1. `inner` is a literal (no `*` / `?`) that `outer` matches — and, when
97
+ * `inner` itself widens to its subpaths (a package pattern under package
98
+ * semantics), `outer` widens too.
99
+ * 2. `outer` is a package pattern under package semantics, and it matches
100
+ * `inner`'s literal prefix up to one of its `/`: every specifier `inner`
101
+ * can match starts with that prefix plus `/`, a subpath `outer` covers.
102
+ *
103
+ * A pattern with an {@link importPatternDefect} proves nothing either way.
104
+ */
105
+ export function importPatternSubsumes(outer, inner, outerMode = 'package', innerMode = outerMode) {
106
+ if (importPatternDefect(outer, outerMode) !== undefined)
107
+ return false;
108
+ if (importPatternDefect(inner, innerMode) !== undefined)
109
+ return false;
110
+ const outerWidens = outerMode === 'package' && isPackagePattern(outer);
111
+ const wild = inner.search(/[*?]/);
112
+ if (wild === -1) {
113
+ if (matchImportPattern(inner, outer, outerMode) === null)
114
+ return false;
115
+ const innerWidens = innerMode === 'package' && isPackagePattern(inner);
116
+ return !innerWidens || outerWidens;
117
+ }
118
+ if (!outerWidens)
119
+ return false;
120
+ const prefix = inner.slice(0, wild);
121
+ for (let k = prefix.indexOf('/'); k > 0; k = prefix.indexOf('/', k + 1)) {
122
+ if (matchImportPattern(prefix.slice(0, k), outer, 'package') !== null)
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ /** Why the dead-unit judge never calls a relative specifier pattern dead — its went-live evidence and the marker refusal both quote it. */
128
+ export const RELATIVE_PATTERN_NEVER_DEAD = 'a relative pattern is never judged dead (it cannot be judged without an importing file)';
129
+ /** Why the dead-unit judge never calls a leading-`*` specifier pattern dead — its went-live evidence and the marker refusal both quote it. */
130
+ export const LEADING_WILDCARD_NEVER_DEAD = 'a leading wildcard could match any known package name, so it is never judged dead';
131
+ /**
132
+ * Could `pattern` match an import of package `name` (or one of its subpaths)?
133
+ * Deliberately permissive — a false "resolvable" costs a missed warning, a false
134
+ * "dead" would cry wolf on a legitimate guard. The evaluator's reach
135
+ * (`resolvedBy`) and {@link importPatternNeverJudgedDead} both read it.
136
+ */
137
+ export function couldMatchPackageName(pattern, name, mode) {
138
+ if (matchImportPattern(name, pattern, mode) !== null)
139
+ return true;
140
+ const wild = pattern.search(/[*?]/);
141
+ if (wild === -1)
142
+ return pattern.startsWith(`${name}/`);
143
+ const prefix = pattern.slice(0, wild);
144
+ if (prefix.length === 0) {
145
+ // A leading `*` could be anything. A leading `?` is exactly ONE character:
146
+ // the literal after the `?` run must still fit the name at that offset —
147
+ // so `?!raw-loader!**` (an inline-loader pattern) can never match a package
148
+ // name, and one that matches no import is a dead unit, not "resolvable"
149
+ // through every known package (round 12 review, R12-DOC-1).
150
+ const lead = pattern.length - pattern.replace(/^\?+/, '').length;
151
+ if (lead === 0)
152
+ return true;
153
+ const literal = pattern.slice(lead).split(/[*?]/)[0] ?? '';
154
+ const rest = name.slice(lead);
155
+ return literal.length === 0 || rest.startsWith(literal) || literal.startsWith(`${rest}/`);
156
+ }
157
+ return name.startsWith(prefix) || prefix.startsWith(`${name}/`);
158
+ }
159
+ /**
160
+ * Why the boundary dead-unit judge can NEVER call `pattern` dead, matched
161
+ * under `mode` — or `undefined` when it can (round 13, K5). Proved from the
162
+ * pattern alone, through the judge's own reach rules (`resolvedBy` in the
163
+ * evaluator): the judge calls a specifier pattern dead only when it resolves to
164
+ * nothing anywhere, and the orchestrator ALWAYS supplies the runtime builtins
165
+ * as known packages (`nodeBuiltinPackageNames`), so these always resolve:
166
+ *
167
+ * - a relative pattern (`./x`, `../legacy/**`) — never judged without an
168
+ * importing file;
169
+ * - a leading `*` — it could match any known package name, and a builtin is
170
+ * always known;
171
+ * - a pattern a runtime builtin module matches (`fs`, `node:*`,
172
+ * `fs/promises`) — that package always exists.
173
+ *
174
+ * A `{ pattern, expectEmpty: true }` marker on such a pattern could only ever
175
+ * read went-live, so the rule validator refuses it at load. A pattern with an
176
+ * {@link importPatternDefect} is answered by the defect instead (`undefined`).
177
+ */
178
+ export function importPatternNeverJudgedDead(pattern, mode = 'package') {
179
+ if (importPatternDefect(pattern, mode) !== undefined)
180
+ return undefined;
181
+ if (pattern.startsWith('.'))
182
+ return RELATIVE_PATTERN_NEVER_DEAD;
183
+ if (pattern.startsWith('*'))
184
+ return LEADING_WILDCARD_NEVER_DEAD;
185
+ const builtins = nodeBuiltinPackageNames();
186
+ const builtin = builtins.find((b) => matchImportPattern(b, pattern, mode) !== null) ??
187
+ builtins.find((b) => couldMatchPackageName(pattern, b, mode));
188
+ return builtin !== undefined
189
+ ? `'${builtin}' is a runtime builtin module — always a known package — so it always resolves and is never judged dead`
190
+ : undefined;
191
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Every module the running runtime ships built in, as an import specifier:
3
+ * each bare name and its `node:` spelling (a name that already carries a
4
+ * scheme — `node:test`, `bun:ffi` — as listed).
5
+ *
6
+ * THE list the boundary plane treats as always-known packages (round 13): the
7
+ * orchestrator's known-package set (`collectKnownPackages`, which the dead-unit
8
+ * judge reads) and the load-time marker refusal (`importPatternNeverJudgedDead`)
9
+ * both read it, so the judge and the refusal can never disagree about whether a
10
+ * builtin-named pattern could ever be judged dead.
11
+ */
12
+ export declare function nodeBuiltinPackageNames(): readonly string[];
13
+ //# sourceMappingURL=node-builtin-package-names.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-builtin-package-names.d.ts","sourceRoot":"","sources":["../../src/scan/node-builtin-package-names.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,IAAI,SAAS,MAAM,EAAE,CAU3D"}
@@ -0,0 +1,25 @@
1
+ import { builtinModules } from 'node:module';
2
+ let cached;
3
+ /**
4
+ * Every module the running runtime ships built in, as an import specifier:
5
+ * each bare name and its `node:` spelling (a name that already carries a
6
+ * scheme — `node:test`, `bun:ffi` — as listed).
7
+ *
8
+ * THE list the boundary plane treats as always-known packages (round 13): the
9
+ * orchestrator's known-package set (`collectKnownPackages`, which the dead-unit
10
+ * judge reads) and the load-time marker refusal (`importPatternNeverJudgedDead`)
11
+ * both read it, so the judge and the refusal can never disagree about whether a
12
+ * builtin-named pattern could ever be judged dead.
13
+ */
14
+ export function nodeBuiltinPackageNames() {
15
+ if (cached === undefined) {
16
+ const out = new Set();
17
+ for (const m of builtinModules) {
18
+ out.add(m);
19
+ if (!m.includes(':'))
20
+ out.add(`node:${m}`);
21
+ }
22
+ cached = [...out];
23
+ }
24
+ return cached;
25
+ }
@@ -1,15 +1,22 @@
1
+ import type { IUnreadFile } from '../util/unread-file.js';
1
2
  export interface IScanImportsOptions {
2
3
  projectRoot: string;
3
4
  extraIgnore?: readonly string[];
4
- /** When set, only files under one of these globs are scanned. */
5
+ /** When set, only files matching one of these globs (project-relative) are scanned. */
5
6
  include?: readonly string[];
7
+ /**
8
+ * Read imports from the RAW text, comments included (`check boundaries
9
+ * --include-comments`). Default `false`: a commented-out import, an import in
10
+ * a doc-comment code fence, or one inside a string literal is not an edge.
11
+ */
12
+ includeComments?: boolean;
6
13
  }
7
14
  export interface IImportEdge {
8
- /** Source file (relative to projectRoot). */
15
+ /** Source file (relative to projectRoot, `/`-separated). */
9
16
  from: string;
10
17
  /** Literal import specifier. */
11
18
  importSpecifier: string;
12
- /** Approximate 1-based line number in the source file. */
19
+ /** 1-based line of the statement's `import` / `export` / `require` keyword. */
13
20
  line: number;
14
21
  /**
15
22
  * Heuristic resolution. v1 sets:
@@ -18,12 +25,38 @@ export interface IImportEdge {
18
25
  * (We do not attempt tsconfig path-mapping resolution here.)
19
26
  */
20
27
  kind: 'internal' | 'external';
28
+ /** True for `import type` / `export type … from` — still a real dependency edge. */
29
+ typeOnly?: boolean;
21
30
  }
22
31
  export interface IImportScanResult {
23
32
  filesScanned: number;
24
33
  edges: IImportEdge[];
25
34
  warnings: string[];
35
+ /**
36
+ * Every scanned source file (project-relative, `/`-separated), INCLUDING files
37
+ * with zero imports. A rule's scope is counted against this list, so a glob
38
+ * that matches only import-free files is still live. Optional for callers
39
+ * that hand-build a scan; the evaluator then falls back to the files that
40
+ * appear in `edges` (and says so in its coverage).
41
+ */
42
+ files?: string[];
43
+ /**
44
+ * Source files the walk matched but could NOT read (a stat or read failure —
45
+ * permissions, or a file vanishing mid-scan), with the reason. They are NOT
46
+ * in `files` and NOT counted in `filesScanned`: a file whose imports were
47
+ * never read was not scanned. `runBoundaryCheck` folds each one into the
48
+ * coverage of every rule whose scope it is in (`readScopeCoverage`), so a
49
+ * rule over an unreadable file settles PARTIAL (2) — never "examined 1 of 1"
50
+ * over the file that held the violation.
51
+ */
52
+ unread?: IUnreadFile[];
53
+ /** Every `package.json` the walk passed (project-relative) — workspace package names. */
54
+ manifestFiles?: string[];
55
+ /** Which text the edges were read from. */
56
+ zone?: 'code' | 'all';
26
57
  }
58
+ /** Drop every memoised file (tests, or a caller that rewrote files in place within one tick). */
59
+ export declare function clearImportScanMemo(): void;
27
60
  /**
28
61
  * Walk the project root and return every detected import edge.
29
62
  */
@@ -1 +1 @@
1
- {"version":3,"file":"scan-imports.d.ts","sourceRoot":"","sources":["../../src/scan/scan-imports.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,iEAAiE;IACjE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAkFD;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,iBAAiB,CAyB3E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,SAAS;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,CAmB7E"}
1
+ {"version":3,"file":"scan-imports.d.ts","sourceRoot":"","sources":["../../src/scan/scan-imports.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAmB1D,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,uFAAuF;IACvF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACvB;AA8ED,iGAAiG;AACjG,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAyBD;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,iBAAiB,CAyE3E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,SAAS;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,CAmB7E"}
@@ -1,5 +1,8 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
+ import { parseImportStatements } from "../extract/parse-imports.js";
4
+ import { UnreadFileReason } from "../util/unread-file-reason.js";
5
+ import { globMayMatchUnder, matchesAny } from "./glob.js";
3
6
  const SUPPORTED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
4
7
  const DEFAULT_IGNORE = new Set([
5
8
  'node_modules',
@@ -13,18 +16,6 @@ const DEFAULT_IGNORE = new Set([
13
16
  '.tmp-pack',
14
17
  '.tmp-smoke-consumer.txt',
15
18
  ]);
16
- // Match `import ... from 'x'`, `export ... from 'x'`, `require('x')`,
17
- // `import('x')` (static and dynamic). Captures the specifier as group 1.
18
- //
19
- // We DELIBERATELY use a single regex per kind rather than a real parser —
20
- // boundary rules need stable behavior across syntaxes and a regex scanner is
21
- // the simplest thing that works for v1. Comments and string escapes can fool
22
- // it; we filter the lowest-hanging fruit (single-line // and /* */ stripped
23
- // per line, but a `// import "x"` line is still ignored).
24
- const IMPORT_RE = /(?:^|\s)(?:import|export)\s+[^'"`]*?from\s+['"]([^'"`]+)['"]/g;
25
- const SIDE_EFFECT_IMPORT_RE = /(?:^|\s)import\s+['"]([^'"`]+)['"]/g;
26
- const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"`]+)['"]\s*\)/g;
27
- const REQUIRE_RE = /\brequire\s*\(\s*['"]([^'"`]+)['"]\s*\)/g;
28
19
  function isIgnored(name, extraIgnore) {
29
20
  if (DEFAULT_IGNORE.has(name))
30
21
  return true;
@@ -39,7 +30,13 @@ function* walk(root, current, extraIgnore) {
39
30
  try {
40
31
  entries = readdirSync(current, { withFileTypes: true });
41
32
  }
42
- catch {
33
+ catch (e) {
34
+ // A directory that vanished mid-walk is out of scope; one that could not
35
+ // be listed (permissions) is in front of every rule that reaches under it.
36
+ const code = e.code;
37
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
38
+ yield { full: current, manifest: false, unlistable: e.message };
39
+ }
43
40
  return;
44
41
  }
45
42
  for (const entry of entries) {
@@ -59,38 +56,55 @@ function* walk(root, current, extraIgnore) {
59
56
  continue;
60
57
  }
61
58
  if (entry.isFile()) {
59
+ if (name === 'package.json') {
60
+ yield { full, manifest: true };
61
+ continue;
62
+ }
62
63
  const ext = nodePath.extname(name);
63
64
  if (!SUPPORTED_EXTS.has(ext))
64
65
  continue;
65
- yield full;
66
+ yield { full, manifest: false };
66
67
  }
67
68
  }
68
69
  }
69
- function lineFor(source, offset) {
70
- let line = 1;
71
- for (let i = 0; i < offset && i < source.length; i += 1) {
72
- if (source[i] === '\n')
73
- line += 1;
74
- }
75
- return line;
70
+ /**
71
+ * Per-process memo of the edges ONE file contributes, keyed on scan root + file
72
+ * + zone and validated by a stat fingerprint (size, mtime, ctime).
73
+ *
74
+ * `scanImports` runs several times per command — the architecture map calls it
75
+ * twice and impact analysis once for a single task-risk report — and for the
76
+ * whole life of the MCP server. Round 11's comment-aware parse (lex → blank
77
+ * comments → match) made re-reading and re-lexing every unchanged file on every
78
+ * call the dominant cost of those reports (~3x). An edited file changes its
79
+ * fingerprint and is re-parsed; an unchanged one is not. Edges are handed out
80
+ * as COPIES so a caller that decorates an edge can never corrupt the memo.
81
+ */
82
+ const EDGE_MEMO = new Map();
83
+ const EDGE_MEMO_LIMIT = 50_000;
84
+ /** Drop every memoised file (tests, or a caller that rewrote files in place within one tick). */
85
+ export function clearImportScanMemo() {
86
+ EDGE_MEMO.clear();
76
87
  }
77
- function extractImports(source, relPath) {
78
- const edges = [];
79
- for (const re of [IMPORT_RE, SIDE_EFFECT_IMPORT_RE, DYNAMIC_IMPORT_RE, REQUIRE_RE]) {
80
- re.lastIndex = 0;
81
- let m;
82
- while ((m = re.exec(source)) !== null) {
83
- const spec = m[1];
84
- const line = lineFor(source, m.index);
85
- edges.push({
86
- from: relPath,
87
- importSpecifier: spec,
88
- line,
89
- kind: spec.startsWith('.') ? 'internal' : 'external',
90
- });
91
- }
92
- }
93
- return edges;
88
+ function toPosix(rel) {
89
+ return nodePath.sep === '/' ? rel : rel.split(nodePath.sep).join('/');
90
+ }
91
+ /**
92
+ * The edges one file contributes — read through THE import parser
93
+ * (`parseImportStatements`), so `check boundaries` and the `import-edges` DSL
94
+ * extractor can never disagree about what a file imports. (Round 11 deleted the
95
+ * four private regexes that used to live here, together with a comment that
96
+ * claimed they stripped comments: no stripping code existed, a commented-out
97
+ * import was a violation, and every import after line 1 was reported one line
98
+ * early.)
99
+ */
100
+ function extractImports(source, relPath, zone) {
101
+ return parseImportStatements(source, { zone }).map((p) => ({
102
+ from: relPath,
103
+ importSpecifier: p.specifier,
104
+ line: p.line,
105
+ kind: p.specifier.startsWith('.') ? 'internal' : 'external',
106
+ ...(p.typeOnly ? { typeOnly: true } : {}),
107
+ }));
94
108
  }
95
109
  /**
96
110
  * Walk the project root and return every detected import edge.
@@ -98,27 +112,82 @@ function extractImports(source, relPath) {
98
112
  export function scanImports(options) {
99
113
  const root = nodePath.resolve(options.projectRoot);
100
114
  const extraIgnore = new Set(options.extraIgnore ?? []);
115
+ const zone = options.includeComments === true ? 'all' : 'code';
116
+ const include = options.include && options.include.length > 0 ? options.include : undefined;
101
117
  const result = {
102
118
  filesScanned: 0,
103
119
  edges: [],
104
120
  warnings: [],
121
+ files: [],
122
+ unread: [],
123
+ manifestFiles: [],
124
+ zone,
105
125
  };
106
126
  if (!existsSync(root)) {
107
127
  result.warnings.push(`scan root does not exist: ${root}`);
108
128
  return result;
109
129
  }
110
- for (const file of walk(root, root, extraIgnore)) {
111
- result.filesScanned += 1;
112
- const rel = nodePath.relative(root, file);
130
+ // A file counts as SCANNED only once its imports were actually read (or
131
+ // served from the memo). It used to be counted — and listed in `files`, the
132
+ // universe every rule's scope is measured against — before the read, so an
133
+ // unreadable file read as "examined 1 of 1" while its imports were never seen.
134
+ const unreadable = (rel, e) => {
135
+ result.warnings.push(`unreadable: ${rel} (${e.message})`);
136
+ result.unread.push({ path: rel, reason: UnreadFileReason.Unreadable });
137
+ };
138
+ for (const hit of walk(root, root, extraIgnore)) {
139
+ const rel = toPosix(nodePath.relative(root, hit.full));
140
+ if (hit.unlistable !== undefined) {
141
+ const dir = rel === '' ? './' : `${rel}/`;
142
+ if (include && !include.some((g) => globMayMatchUnder(g, dir)))
143
+ continue;
144
+ result.warnings.push(`unlistable directory: ${dir} (${hit.unlistable})`);
145
+ result.unread.push({ path: dir, reason: UnreadFileReason.UnreadableDirectory });
146
+ continue;
147
+ }
148
+ if (hit.manifest) {
149
+ result.manifestFiles.push(rel);
150
+ continue;
151
+ }
152
+ if (include && !matchesAny(rel, include))
153
+ continue;
154
+ let fingerprint;
155
+ try {
156
+ const st = statSync(hit.full);
157
+ fingerprint = `${st.size}:${st.mtimeMs}:${st.ctimeMs}`;
158
+ }
159
+ catch (e) {
160
+ // Deleted since the walk saw it: gone, so out of scope (the one reader's
161
+ // rule, `readMatchingFiles`). Anything else leaves it unexamined.
162
+ if (e.code !== 'ENOENT')
163
+ unreadable(rel, e);
164
+ continue;
165
+ }
166
+ const key = `${root}\0${hit.full}\0${zone}`;
167
+ const memo = EDGE_MEMO.get(key);
168
+ if (memo && memo.fingerprint === fingerprint) {
169
+ result.filesScanned += 1;
170
+ result.files.push(rel);
171
+ for (const e of memo.edges)
172
+ result.edges.push({ ...e });
173
+ continue;
174
+ }
113
175
  let source;
114
176
  try {
115
- source = readFileSync(file, 'utf8');
177
+ source = readFileSync(hit.full, 'utf8');
116
178
  }
117
179
  catch (e) {
118
- result.warnings.push(`unreadable: ${rel} (${e.message})`);
180
+ unreadable(rel, e);
119
181
  continue;
120
182
  }
121
- result.edges.push(...extractImports(source, rel));
183
+ result.filesScanned += 1;
184
+ result.files.push(rel);
185
+ const edges = extractImports(source, rel, zone);
186
+ if (EDGE_MEMO.size >= EDGE_MEMO_LIMIT)
187
+ EDGE_MEMO.clear();
188
+ EDGE_MEMO.set(key, { fingerprint, edges });
189
+ for (const e of edges)
190
+ result.edges.push({ ...e });
122
191
  }
123
192
  return result;
124
193
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * One blank-run backtracking hazard found in a regex source by
3
+ * `findBlankRunHazards`.
4
+ *
5
+ * `leading`: an alternative can START a match at every offset of a blank run
6
+ * and then consume the rest of the run — O(run²) per blanked comment/string.
7
+ * `adjacent`: two whitespace-consuming quantifiers touch (only optional items
8
+ * between them), so the engine re-partitions every run between them.
9
+ */
10
+ export interface IBlankRunHazard {
11
+ readonly shape: 'leading' | 'adjacent';
12
+ /** Offset of the offending construct in the regex source. */
13
+ readonly index: number;
14
+ /** The offending slice of the source. */
15
+ readonly fragment: string;
16
+ /** One sentence: what backtracks, and why a blanked buffer makes it expensive. */
17
+ readonly message: string;
18
+ /**
19
+ * The hazard's REACH: whether the flagged quantifier(s) consume newlines.
20
+ *
21
+ * A line-bounded hazard (`[ \t]*`, `.*?`, `[ ]*` — or an `adjacent` pair
22
+ * overlapping only on spaces) re-scans at most to the end of its line, so
23
+ * on a blanked buffer it costs Σ line-run², not Σ (multi-line run)². A
24
+ * cost prediction that ignores this skips files the pattern would scan in
25
+ * milliseconds.
26
+ */
27
+ readonly crossesNewline: boolean;
28
+ /**
29
+ * `leading` only: a match can start only at a LINE start (`(?:^|\n)\s*`),
30
+ * so the cost is (lines × run), not run².
31
+ */
32
+ readonly fromLineStart?: boolean;
33
+ }
34
+ //# sourceMappingURL=blank-run-hazard-finding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blank-run-hazard-finding.d.ts","sourceRoot":"","sources":["../../src/util/blank-run-hazard-finding.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IACvC,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { IBlankRunHazard } from './blank-run-hazard-finding.js';
2
+ /**
3
+ * Static lint: does this regex backtrack quadratically on a BLANKED buffer?
4
+ *
5
+ * A non-`all` `scan` zone blanks comments and strings into runs of spaces
6
+ * (newlines kept, so line numbers stay true). A pattern that is fast on real
7
+ * source becomes O(run²) on such a buffer when:
8
+ *
9
+ * - (leading) an alternative's first mandatory construct — after only
10
+ * optional groups or non-restricting zero-width atoms — is a
11
+ * whitespace-consuming quantifier. Every offset of a run is a start, and
12
+ * each start re-scans the rest of the run. A start restricted to newlines
13
+ * (`(?:^|\n)`) is only a hazard when the quantifier also crosses newlines
14
+ * (`\s*`); `[ \t]*` then stops at the end of its line.
15
+ * - (adjacent) two whitespace-consuming quantifiers touch, separated only by
16
+ * optional items or whitespace-capable atoms — `\s*(?:<x>)?\s*=`, a lazy
17
+ * `[^;'"]*?` next to `\s*` — so the engine re-partitions each run between
18
+ * them.
19
+ *
20
+ * Measured: one such pattern took 3 ms on raw text and 5,402 ms on the
21
+ * blanked buffer of the same file. This cannot be caught by tests over
22
+ * ordinary source; it is caught here, from the pattern text, before it runs.
23
+ *
24
+ * Deterministic and advisory: a small tokenizer over the regex source, no
25
+ * regex engine involved. It over-approximates (lookarounds are treated as
26
+ * transparent; flags are ignored), which is the right direction for a hint.
27
+ */
28
+ export declare function findBlankRunHazards(source: string): readonly IBlankRunHazard[];
29
+ //# sourceMappingURL=blank-run-hazard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blank-run-hazard.d.ts","sourceRoot":"","sources":["../../src/util/blank-run-hazard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,eAAe,EAAE,CAY9E"}