@shrkcrft/boundaries 0.1.0-alpha.29 → 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 (229) 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 +10 -8
  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 +49 -1
  35. package/dist/extract/extract-tokens.d.ts.map +1 -1
  36. package/dist/extract/extract-tokens.js +283 -25
  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 +50 -0
  41. package/dist/extract/import-edges.d.ts.map +1 -0
  42. package/dist/extract/import-edges.js +170 -0
  43. package/dist/extract/inspect-source.d.ts +38 -3
  44. package/dist/extract/inspect-source.d.ts.map +1 -1
  45. package/dist/extract/inspect-source.js +56 -8
  46. package/dist/extract/parse-imports.d.ts +80 -0
  47. package/dist/extract/parse-imports.d.ts.map +1 -0
  48. package/dist/extract/parse-imports.js +254 -0
  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 +20 -2
  56. package/dist/generated/check-provenance.d.ts.map +1 -1
  57. package/dist/generated/check-provenance.js +27 -6
  58. package/dist/generated/read-regen-tree.d.ts +16 -0
  59. package/dist/generated/read-regen-tree.d.ts.map +1 -0
  60. package/dist/generated/read-regen-tree.js +61 -0
  61. package/dist/generated/scan-generated.d.ts +57 -3
  62. package/dist/generated/scan-generated.d.ts.map +1 -1
  63. package/dist/generated/scan-generated.js +111 -11
  64. package/dist/index.d.ts +51 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +54 -0
  67. package/dist/model/boundary-intended-empty-line.d.ts +10 -0
  68. package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
  69. package/dist/model/boundary-intended-empty-line.js +11 -0
  70. package/dist/model/boundary-markable-list.d.ts +15 -0
  71. package/dist/model/boundary-markable-list.d.ts.map +1 -0
  72. package/dist/model/boundary-markable-list.js +15 -0
  73. package/dist/model/boundary-rule-input-keys.d.ts +11 -0
  74. package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
  75. package/dist/model/boundary-rule-input-keys.js +29 -0
  76. package/dist/model/boundary-rule-input.d.ts +23 -0
  77. package/dist/model/boundary-rule-input.d.ts.map +1 -0
  78. package/dist/model/boundary-rule-input.js +1 -0
  79. package/dist/model/boundary-rule-key-problems.d.ts +11 -0
  80. package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
  81. package/dist/model/boundary-rule-key-problems.js +36 -0
  82. package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
  83. package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
  84. package/dist/model/boundary-rule-marker-problems.js +94 -0
  85. package/dist/model/boundary-rule-scope.d.ts +88 -0
  86. package/dist/model/boundary-rule-scope.d.ts.map +1 -0
  87. package/dist/model/boundary-rule-scope.js +175 -0
  88. package/dist/model/boundary-rule.d.ts +120 -5
  89. package/dist/model/boundary-rule.d.ts.map +1 -1
  90. package/dist/model/boundary-rule.js +148 -1
  91. package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
  92. package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
  93. package/dist/model/boundary-unit-problem-issue.js +16 -0
  94. package/dist/model/normalize-boundary-rule.d.ts +21 -0
  95. package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
  96. package/dist/model/normalize-boundary-rule.js +62 -0
  97. package/dist/policy/evaluate-policy.d.ts +80 -3
  98. package/dist/policy/evaluate-policy.d.ts.map +1 -1
  99. package/dist/policy/evaluate-policy.js +152 -19
  100. package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
  101. package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
  102. package/dist/policy/i-policy-rule-liveness.js +1 -0
  103. package/dist/policy/run-policy.d.ts +1 -1
  104. package/dist/policy/run-policy.d.ts.map +1 -1
  105. package/dist/policy/run-policy.js +171 -21
  106. package/dist/registry/load-boundary-rules.d.ts +29 -1
  107. package/dist/registry/load-boundary-rules.d.ts.map +1 -1
  108. package/dist/registry/load-boundary-rules.js +38 -8
  109. package/dist/scan/glob.d.ts +90 -0
  110. package/dist/scan/glob.d.ts.map +1 -1
  111. package/dist/scan/glob.js +205 -0
  112. package/dist/scan/i-glob-unit-measure.d.ts +22 -0
  113. package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
  114. package/dist/scan/i-glob-unit-measure.js +1 -0
  115. package/dist/scan/import-pattern.d.ts +109 -0
  116. package/dist/scan/import-pattern.d.ts.map +1 -0
  117. package/dist/scan/import-pattern.js +191 -0
  118. package/dist/scan/node-builtin-package-names.d.ts +13 -0
  119. package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
  120. package/dist/scan/node-builtin-package-names.js +25 -0
  121. package/dist/scan/scan-imports.d.ts +36 -3
  122. package/dist/scan/scan-imports.d.ts.map +1 -1
  123. package/dist/scan/scan-imports.js +114 -45
  124. package/dist/util/blank-run-hazard-finding.d.ts +34 -0
  125. package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
  126. package/dist/util/blank-run-hazard-finding.js +1 -0
  127. package/dist/util/blank-run-hazard.d.ts +29 -0
  128. package/dist/util/blank-run-hazard.d.ts.map +1 -0
  129. package/dist/util/blank-run-hazard.js +450 -0
  130. package/dist/util/dead-glob-units.d.ts +46 -0
  131. package/dist/util/dead-glob-units.d.ts.map +1 -0
  132. package/dist/util/dead-glob-units.js +116 -0
  133. package/dist/util/glob-list-liveness-input.d.ts +31 -0
  134. package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
  135. package/dist/util/glob-list-liveness-input.js +65 -0
  136. package/dist/util/i-dead-glob-unit.d.ts +24 -0
  137. package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
  138. package/dist/util/i-dead-glob-unit.js +1 -0
  139. package/dist/util/i-glob-list-units.d.ts +25 -0
  140. package/dist/util/i-glob-list-units.d.ts.map +1 -0
  141. package/dist/util/i-glob-list-units.js +1 -0
  142. package/dist/util/i-glob-liveness-list.d.ts +23 -0
  143. package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
  144. package/dist/util/i-glob-liveness-list.js +1 -0
  145. package/dist/util/i-glob-liveness-request.d.ts +11 -0
  146. package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
  147. package/dist/util/i-glob-liveness-request.js +1 -0
  148. package/dist/util/i-glob-negation.d.ts +13 -0
  149. package/dist/util/i-glob-negation.d.ts.map +1 -0
  150. package/dist/util/i-glob-negation.js +1 -0
  151. package/dist/util/matched-files.d.ts +22 -0
  152. package/dist/util/matched-files.d.ts.map +1 -0
  153. package/dist/util/matched-files.js +1 -0
  154. package/dist/util/negation-cause.d.ts +15 -0
  155. package/dist/util/negation-cause.d.ts.map +1 -0
  156. package/dist/util/negation-cause.js +21 -0
  157. package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
  158. package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
  159. package/dist/util/plane-scan-exclude-dirs.js +22 -0
  160. package/dist/util/read-glob-list-liveness.d.ts +20 -0
  161. package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
  162. package/dist/util/read-glob-list-liveness.js +20 -0
  163. package/dist/util/read-scope-coverage.d.ts +90 -0
  164. package/dist/util/read-scope-coverage.d.ts.map +1 -0
  165. package/dist/util/read-scope-coverage.js +173 -0
  166. package/dist/util/read-scope.d.ts +14 -0
  167. package/dist/util/read-scope.d.ts.map +1 -0
  168. package/dist/util/read-scope.js +1 -0
  169. package/dist/util/read-selected-files.d.ts +16 -0
  170. package/dist/util/read-selected-files.d.ts.map +1 -0
  171. package/dist/util/read-selected-files.js +25 -0
  172. package/dist/util/settle-glob-lists.d.ts +12 -0
  173. package/dist/util/settle-glob-lists.d.ts.map +1 -0
  174. package/dist/util/settle-glob-lists.js +13 -0
  175. package/dist/util/unread-file-reason.d.ts +26 -0
  176. package/dist/util/unread-file-reason.d.ts.map +1 -0
  177. package/dist/util/unread-file-reason.js +26 -0
  178. package/dist/util/unread-file.d.ts +10 -0
  179. package/dist/util/unread-file.d.ts.map +1 -0
  180. package/dist/util/unread-file.js +1 -0
  181. package/dist/util/walk-files.d.ts +90 -7
  182. package/dist/util/walk-files.d.ts.map +1 -1
  183. package/dist/util/walk-files.js +206 -19
  184. package/dist/wiring/evaluate-wiring.d.ts +88 -7
  185. package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
  186. package/dist/wiring/evaluate-wiring.js +276 -16
  187. package/dist/wiring/explain-wiring.d.ts +56 -14
  188. package/dist/wiring/explain-wiring.d.ts.map +1 -1
  189. package/dist/wiring/explain-wiring.js +136 -10
  190. package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
  191. package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
  192. package/dist/wiring/i-idiom-role-coverage.js +1 -0
  193. package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
  194. package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
  195. package/dist/wiring/i-registration-query-verdict.js +1 -0
  196. package/dist/wiring/i-registration-roles.d.ts +71 -0
  197. package/dist/wiring/i-registration-roles.d.ts.map +1 -0
  198. package/dist/wiring/i-registration-roles.js +1 -0
  199. package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
  200. package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
  201. package/dist/wiring/measure-idiom-role-coverage.js +20 -0
  202. package/dist/wiring/measure-registration-roles.d.ts +25 -0
  203. package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
  204. package/dist/wiring/measure-registration-roles.js +124 -0
  205. package/dist/wiring/plan-wiring-fix.d.ts +16 -1
  206. package/dist/wiring/plan-wiring-fix.d.ts.map +1 -1
  207. package/dist/wiring/plan-wiring-fix.js +177 -4
  208. package/dist/wiring/registration-graph.d.ts +13 -1
  209. package/dist/wiring/registration-graph.d.ts.map +1 -1
  210. package/dist/wiring/registration-graph.js +22 -6
  211. package/dist/wiring/registration-query-verdict.d.ts +23 -0
  212. package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
  213. package/dist/wiring/registration-query-verdict.js +118 -0
  214. package/dist/wiring/registry-query.d.ts +9 -1
  215. package/dist/wiring/registry-query.d.ts.map +1 -1
  216. package/dist/wiring/registry-query.js +17 -6
  217. package/dist/wiring/scan-wiring-files.d.ts +5 -3
  218. package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
  219. package/dist/wiring/scan-wiring-files.js +55 -14
  220. package/dist/wiring/sink-imports.d.ts +95 -0
  221. package/dist/wiring/sink-imports.d.ts.map +1 -0
  222. package/dist/wiring/sink-imports.js +124 -0
  223. package/dist/wiring/trace-literal.d.ts +6 -0
  224. package/dist/wiring/trace-literal.d.ts.map +1 -1
  225. package/dist/wiring/trace-literal.js +5 -2
  226. package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
  227. package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
  228. package/dist/wiring/wiring-labeled-sources.js +20 -0
  229. package/package.json +2 -2
@@ -0,0 +1,170 @@
1
+ import { globListSelects } from "../scan/glob.js";
2
+ import { resolveAliasCandidates } from "../scan/tsconfig-aliases.js";
3
+ import { safeCompile } from "../util/safe-regex.js";
4
+ import { parseImportStatementsWithMeta } from "./parse-imports.js";
5
+ /** Normalize a relative specifier against the importing file's directory. */
6
+ function resolveRelative(fromFile, specifier) {
7
+ const dir = fromFile.includes('/') ? fromFile.slice(0, fromFile.lastIndexOf('/')) : '';
8
+ const stack = dir.split('/').filter(Boolean);
9
+ for (const part of specifier.split('/')) {
10
+ if (part === '' || part === '.')
11
+ continue;
12
+ if (part === '..') {
13
+ stack.pop();
14
+ continue;
15
+ }
16
+ stack.push(part);
17
+ }
18
+ return stack.join('/');
19
+ }
20
+ /**
21
+ * Resolve a specifier the way the boundary engine does: relative paths against
22
+ * the importing file, bare specifiers through the tsconfig alias map, and
23
+ * anything left over kept as the literal package name.
24
+ */
25
+ function resolveSpecifier(fromFile, specifier, aliases) {
26
+ if (specifier.startsWith('.')) {
27
+ return { specifier, path: resolveRelative(fromFile, specifier) };
28
+ }
29
+ if (aliases) {
30
+ const candidates = resolveAliasCandidates(specifier, aliases);
31
+ // The first candidate is the one the compiler would try first; keeping only
32
+ // it makes the emitted id deterministic rather than order-dependent.
33
+ if (candidates.length > 0)
34
+ return { specifier, path: candidates[0] };
35
+ }
36
+ return { specifier };
37
+ }
38
+ /** Whether a resolved specifier is one the rule's `to` selects. */
39
+ function matchesTarget(resolved, to, modulePattern) {
40
+ const hasSelector = to.module !== undefined || to.modulePattern !== undefined || (to.files?.length ?? 0) > 0;
41
+ // No target selector at all means "every import" — useful when the rule
42
+ // narrows by symbol alone.
43
+ if (!hasSelector)
44
+ return true;
45
+ if (to.module !== undefined) {
46
+ // Exact package, or a subpath of it: `@x/generated` selects
47
+ // `@x/generated` and `@x/generated/views` but never `@x/generated-legacy`.
48
+ if (resolved.specifier === to.module || resolved.specifier.startsWith(`${to.module}/`)) {
49
+ return true;
50
+ }
51
+ }
52
+ if (modulePattern) {
53
+ modulePattern.lastIndex = 0;
54
+ if (modulePattern.test(resolved.specifier))
55
+ return true;
56
+ }
57
+ if (to.files && to.files.length > 0 && resolved.path !== undefined) {
58
+ // `to.files` is a glob LIST like any other: a `!` entry subtracts.
59
+ if (globListSelects(resolved.path, to.files))
60
+ return true;
61
+ // Specifiers routinely omit the extension, so probe the usual endings
62
+ // rather than forcing every config to spell `*.{ts,tsx,…}` itself.
63
+ for (const ext of ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx']) {
64
+ if (globListSelects(resolved.path + ext, to.files))
65
+ return true;
66
+ if (globListSelects(`${resolved.path}/index${ext}`, to.files))
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+ /**
73
+ * Extract the import edges a source selects.
74
+ *
75
+ * `files` is the CONSUMER glob (the `from` side): it reuses the field every
76
+ * other extractor uses, so `--changed-only` footprinting, the shared walk, and
77
+ * `$use` all work on it unchanged.
78
+ */
79
+ export function extractImportEdges(source, files, context = {}) {
80
+ const to = source.to ?? {};
81
+ const emit = source.emit ?? 'edge';
82
+ let modulePattern;
83
+ if (to.modulePattern !== undefined) {
84
+ const compiled = safeCompile(to.modulePattern, to.modulePatternFlags);
85
+ if (compiled.error || !compiled.re)
86
+ return { sites: [], error: `to.modulePattern ${compiled.error}` };
87
+ modulePattern = compiled.re;
88
+ }
89
+ let symbolPattern;
90
+ if (to.match !== undefined) {
91
+ const compiled = safeCompile(to.match, to.matchFlags);
92
+ if (compiled.error || !compiled.re)
93
+ return { sites: [], error: `to.match ${compiled.error}` };
94
+ symbolPattern = compiled.re;
95
+ }
96
+ // Zoning is judged by where the statement's KEYWORD starts and whether its
97
+ // specifier is a real string — the same authority `check boundaries` uses
98
+ // (round 11, 6.1a#import-edges-scan). The extractor receives RAW content:
99
+ // pre-blanking with `scan: 'code'` used to erase every specifier (a string)
100
+ // and turn the rule vacuous. Unset → comment-aware, matching `check
101
+ // boundaries`; `scan: 'all'` is the raw escape hatch. (`strings` / `comments`
102
+ // are rejected at config load: an import statement is code.)
103
+ const zone = source.scan === 'all' ? 'all' : 'code';
104
+ const sites = [];
105
+ let blankedChars = 0;
106
+ for (const file of files) {
107
+ const parsed = parseImportStatementsWithMeta(file.content, { zone });
108
+ blankedChars += parsed.blankedChars;
109
+ for (const statement of parsed.statements) {
110
+ const resolved = resolveSpecifier(file.path, statement.specifier, context.tsconfigPaths);
111
+ if (!matchesTarget(resolved, to, modulePattern))
112
+ continue;
113
+ const symbols = statement.bindings
114
+ .map((b) => b.imported ?? b.local)
115
+ .filter((name) => {
116
+ if (!symbolPattern)
117
+ return true;
118
+ symbolPattern.lastIndex = 0;
119
+ return symbolPattern.test(name);
120
+ });
121
+ // A symbol filter that matched nothing means this statement is not a hit,
122
+ // even though its module was: the rule asked about specific symbols.
123
+ if (symbolPattern && symbols.length === 0)
124
+ continue;
125
+ if (emit === 'from') {
126
+ sites.push({ token: file.path, file: file.path, line: statement.line });
127
+ continue;
128
+ }
129
+ if (symbols.length === 0) {
130
+ // A side-effect or type-only import binds no name; the edge is still a
131
+ // real dependency, so it is reported against the module it names.
132
+ if (emit === 'symbol')
133
+ continue;
134
+ sites.push({
135
+ token: `${file.path} → ${resolved.specifier}`,
136
+ file: file.path,
137
+ line: statement.line,
138
+ });
139
+ continue;
140
+ }
141
+ for (const symbol of symbols) {
142
+ sites.push({
143
+ token: emit === 'symbol' ? symbol : `${file.path} → ${symbol}`,
144
+ file: file.path,
145
+ line: statement.line,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ // The one place a user's intuition reliably trips: `to.files` matches an
151
+ // import's DIRECTLY-resolved path, and a symbol re-exported through a barrel
152
+ // resolves to the package entry, not the deep file. "Edges to files in
153
+ // generated/**" then finds nothing, correctly but unhelpfully. Detecting the
154
+ // exact shape of that dead end and naming the fix costs one conditional.
155
+ //
156
+ // Narrow on purpose. A rule that ALREADY targets by module has been told to
157
+ // use the thing it is using, which is worse than saying nothing — so the
158
+ // hint fires only when `to.files` is the sole target selector.
159
+ const targetsByFilesOnly = (to.files?.length ?? 0) > 0 && to.module === undefined && to.modulePattern === undefined;
160
+ if (sites.length === 0 && files.length > 0 && targetsByFilesOnly) {
161
+ return {
162
+ sites,
163
+ hint: '0 edges via `to.files` — that matches an import\'s DIRECTLY-resolved path, so a symbol ' +
164
+ 're-exported through a barrel/package resolves to the package entry, not the deep file. ' +
165
+ 'Target by `to.module` + `to.match` instead.',
166
+ blankedChars,
167
+ };
168
+ }
169
+ return { sites, blankedChars };
170
+ }
@@ -1,15 +1,29 @@
1
- import type { IWiringSource } from '@shrkcrft/core';
1
+ import { type IWiringSource } from '@shrkcrft/core';
2
+ import type { IGlobListUnits } from '../util/i-glob-list-units.js';
3
+ import type { IUnreadFile } from '../util/unread-file.js';
2
4
  import { type IExtractedSite } from './extract-tokens.js';
3
5
  /** What one source actually resolved to against the live tree. */
4
6
  export interface ISourceInspection {
5
- /** Files the globs matched (after the shared walk's skip rules). */
7
+ /** Files the globs matched and the reader READ (after the shared walk's skip rules). */
6
8
  readonly filesScanned: number;
9
+ /**
10
+ * Files the globs matched that the reader did NOT read (over the read cap,
11
+ * or unreadable). A source with any is not "matched nothing" and not fully
12
+ * examined: its coverage names them (`readScopeCoverage`).
13
+ */
14
+ readonly unread: readonly IUnreadFile[];
7
15
  /** Distinct ids extracted, sorted. */
8
16
  readonly ids: readonly string[];
9
17
  /** Every capture site, in stable (file, line) order. */
10
18
  readonly sites: readonly IExtractedSite[];
11
19
  /** Set when the source is misconfigured. */
12
20
  readonly error?: string;
21
+ /**
22
+ * A diagnosis for a zero-match that is technically correct but almost
23
+ * certainly not what the author meant — surfaced alongside the loud skip so
24
+ * the dead end explains itself.
25
+ */
26
+ readonly hint?: string;
13
27
  }
14
28
  /**
15
29
  * Resolve ONE source against the tree and report what it matched.
@@ -20,5 +34,26 @@ export interface ISourceInspection {
20
34
  * extract?" has one answer computed one way. A rule that matched 0 is then a
21
35
  * fact the tooling can report, not something an author has to notice.
22
36
  */
23
- export declare function inspectSource(projectRoot: string, source: IWiringSource, excludeDirs?: readonly string[]): ISourceInspection;
37
+ export declare function inspectSource(projectRoot: string, rawSource: IWiringSource, excludeDirs?: readonly string[]): ISourceInspection;
38
+ /**
39
+ * ONE source's glob units: the dead ones (each with its reason), the live
40
+ * negations with what each excludes, and how many globs were checked — through
41
+ * the one dead-unit decision, `globListUnits`.
42
+ *
43
+ * It reads the same walk {@link inspectSource} reads (inside
44
+ * `withFileReadCache` a second call over the same globs is a memo hit), so the
45
+ * answer is about the files the source actually SEES — after `SKIP_DIRS` and
46
+ * `excludeDirs`. The walk is the list's POSITIVE set (every file an inclusion
47
+ * glob matched), so a negation is judged by what it removes from it, never by
48
+ * what it "matches". A file over the read cap was still MATCHED, so a glob that
49
+ * matched only such a file is not dead. Only `files[]` walk globs count: an
50
+ * `import-edges` `to.files` matches resolved specifiers, not walked files.
51
+ */
52
+ export declare function sourceGlobUnits(projectRoot: string, source: IWiringSource, excludeDirs?: readonly string[]): IGlobListUnits;
53
+ /**
54
+ * The globs of ONE source that do nothing — a dead unit inside a rule whose
55
+ * other globs may still keep it connected. The labels of
56
+ * {@link sourceGlobUnits}`.dead` (a dead negation keeps its `!`).
57
+ */
58
+ export declare function sourceDeadGlobs(projectRoot: string, source: IWiringSource, excludeDirs?: readonly string[]): readonly string[];
24
59
  //# sourceMappingURL=inspect-source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inspect-source.d.ts","sourceRoot":"","sources":["../../src/extract/inspect-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEzE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sCAAsC;IACtC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,iBAAiB,CAgBnB"}
1
+ {"version":3,"file":"inspect-source.d.ts","sourceRoot":"","sources":["../../src/extract/inspect-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGzE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,wFAAwF;IACxF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,sCAAsC;IACtC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,aAAa,EACxB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,iBAAiB,CAyBnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,cAAc,CAEhB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,SAAS,MAAM,EAAE,CAEnB"}
@@ -1,6 +1,8 @@
1
- import { matchesAny } from "../scan/glob.js";
2
- import { readMatchingFiles } from "../util/walk-files.js";
1
+ import { normalizeWiringSource } from '@shrkcrft/core';
2
+ import { readSelectedFiles } from "../util/read-selected-files.js";
3
+ import { readGlobListUnits } from "../util/dead-glob-units.js";
3
4
  import { extractTokens } from "./extract-tokens.js";
5
+ import { loadTsconfigPaths } from "../scan/tsconfig-aliases.js";
4
6
  /**
5
7
  * Resolve ONE source against the tree and report what it matched.
6
8
  *
@@ -10,18 +12,64 @@ import { extractTokens } from "./extract-tokens.js";
10
12
  * extract?" has one answer computed one way. A rule that matched 0 is then a
11
13
  * fact the tooling can report, not something an author has to notice.
12
14
  */
13
- export function inspectSource(projectRoot, source, excludeDirs = []) {
15
+ export function inspectSource(projectRoot, rawSource, excludeDirs = []) {
16
+ // The engine entry normalises idempotently (round 13): a loaded source comes
17
+ // back equal; a hand-built one with a `{ pattern, expectEmpty }` entry reads
18
+ // its pattern as the glob, and a malformed entry is a misconfiguration —
19
+ // never a glob reader handed an object.
20
+ const normalized = normalizeWiringSource(rawSource);
21
+ if (!normalized.ok)
22
+ return { filesScanned: 0, unread: [], ids: [], sites: [], error: normalized.error.message };
23
+ const source = normalized.value;
14
24
  const globs = source.files ?? [];
15
- const cache = readMatchingFiles(projectRoot, globs, new Set(excludeDirs));
16
- const files = [...cache.entries()]
17
- .filter(([path]) => matchesAny(path, globs))
18
- .map(([path, content]) => ({ path, content }));
19
- const res = extractTokens(source, files);
25
+ const { files, unread } = sourceFiles(projectRoot, globs, excludeDirs);
26
+ // `import-edges` resolves alias specifiers the way the compiler would, so it
27
+ // needs the project's tsconfig paths. Loading it here (rather than inside the
28
+ // extractor) keeps every extractor a pure function of its inputs.
29
+ const res = extractTokens(source, files, { tsconfigPaths: loadTsconfigPaths(projectRoot) });
20
30
  const sites = [...res.sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.token.localeCompare(b.token));
21
31
  return {
22
32
  filesScanned: files.length,
33
+ unread,
23
34
  ids: [...new Set(sites.map((s) => s.token))].sort(),
24
35
  sites,
25
36
  ...(res.error ? { error: res.error } : {}),
37
+ ...(res.hint ? { hint: res.hint } : {}),
38
+ };
39
+ }
40
+ /**
41
+ * ONE source's glob units: the dead ones (each with its reason), the live
42
+ * negations with what each excludes, and how many globs were checked — through
43
+ * the one dead-unit decision, `globListUnits`.
44
+ *
45
+ * It reads the same walk {@link inspectSource} reads (inside
46
+ * `withFileReadCache` a second call over the same globs is a memo hit), so the
47
+ * answer is about the files the source actually SEES — after `SKIP_DIRS` and
48
+ * `excludeDirs`. The walk is the list's POSITIVE set (every file an inclusion
49
+ * glob matched), so a negation is judged by what it removes from it, never by
50
+ * what it "matches". A file over the read cap was still MATCHED, so a glob that
51
+ * matched only such a file is not dead. Only `files[]` walk globs count: an
52
+ * `import-edges` `to.files` matches resolved specifiers, not walked files.
53
+ */
54
+ export function sourceGlobUnits(projectRoot, source, excludeDirs = []) {
55
+ return readGlobListUnits(projectRoot, source.files ?? [], new Set(excludeDirs));
56
+ }
57
+ /**
58
+ * The globs of ONE source that do nothing — a dead unit inside a rule whose
59
+ * other globs may still keep it connected. The labels of
60
+ * {@link sourceGlobUnits}`.dead` (a dead negation keeps its `!`).
61
+ */
62
+ export function sourceDeadGlobs(projectRoot, source, excludeDirs = []) {
63
+ return sourceGlobUnits(projectRoot, source, excludeDirs).dead.map((u) => u.glob);
64
+ }
65
+ /**
66
+ * The files a source's glob list SELECTS (an inclusion glob matches, no
67
+ * negation does), after the shared walk's skip rules: read, and unread.
68
+ */
69
+ function sourceFiles(projectRoot, globs, excludeDirs) {
70
+ const selected = readSelectedFiles(projectRoot, globs, new Set(excludeDirs));
71
+ return {
72
+ files: [...selected.files.entries()].map(([path, content]) => ({ path, content })),
73
+ unread: [...selected.unread],
26
74
  };
27
75
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Which text an import statement may be read from.
3
+ *
4
+ * - `code` (the default): the statement's keyword must start in executable
5
+ * code and its specifier must be a real string literal; comments are
6
+ * blanked before matching, so an apostrophe inside a comment within a
7
+ * multi-line clause can no longer cut a real import short.
8
+ * - `all`: the raw file text — every byte, comments included. The explicit
9
+ * escape hatch (`check boundaries --include-comments`, `scan: 'all'`).
10
+ */
11
+ export type ImportParseZone = 'code' | 'all';
12
+ /** Options for {@link parseImportStatements}. */
13
+ export interface IParseImportsOptions {
14
+ /** Default `code`. */
15
+ readonly zone?: ImportParseZone;
16
+ }
17
+ /** How a name entered scope through an import. */
18
+ export type ImportBindingKind = 'named' | 'default' | 'namespace';
19
+ /** One name bound by an import statement. */
20
+ export interface IImportBinding {
21
+ /** The name as used in the file (after any `as` alias). */
22
+ readonly local: string;
23
+ readonly kind: ImportBindingKind;
24
+ /** The exported name, for a named import (differs from `local` when aliased). */
25
+ readonly imported?: string;
26
+ }
27
+ /**
28
+ * How the dependency was expressed.
29
+ *
30
+ * `--fix` only ever extends the STATIC import block, so it filters on this;
31
+ * the dependency graph counts every kind, because a fence that missed
32
+ * `await import('../secret')` would have a hole exactly where someone
33
+ * motivated would put one.
34
+ */
35
+ export type ImportStatementKind = 'import' | 'reexport' | 'side-effect' | 'dynamic' | 'require';
36
+ /** One parsed dependency statement. */
37
+ export interface IParsedImport {
38
+ readonly specifier: string;
39
+ readonly bindings: readonly IImportBinding[];
40
+ /** 1-based line of the statement's start. */
41
+ readonly line: number;
42
+ /** Character offset of the statement's start. */
43
+ readonly index: number;
44
+ /** The matched statement text, for style detection (quotes, semicolons). */
45
+ readonly raw: string;
46
+ /** True for `import type { … }` — binds no runtime value. */
47
+ readonly typeOnly: boolean;
48
+ readonly kind: ImportStatementKind;
49
+ }
50
+ /**
51
+ * Every import / re-export statement in `content`, with the names it binds.
52
+ *
53
+ * A `import type { … }` statement is returned with `typeOnly: true` rather than
54
+ * dropped: the `--fix` planner must ignore it (it binds no value), while the
55
+ * dependency graph legitimately counts it as an edge. Returning it and letting
56
+ * each caller decide keeps that judgement at the call site instead of baking
57
+ * one feature's answer into the shared parser.
58
+ *
59
+ * In the default `code` zone a statement is kept only when its keyword starts
60
+ * in a code zone AND its specifier quote opens a real string literal — the
61
+ * same {@link zoneKeepsAt} authority the policy plane and the extraction DSL
62
+ * use. Offsets, lines and `raw` always refer to the ORIGINAL text (blanking
63
+ * preserves length), so callers that splice `content` stay correct.
64
+ */
65
+ export declare function parseImportStatements(content: string, options?: IParseImportsOptions): IParsedImport[];
66
+ /**
67
+ * {@link parseImportStatements}, plus how many characters the zone BLANKED
68
+ * before matching: the comment characters under `code`, `0` under `all`.
69
+ *
70
+ * The `import-edges` extractor zones itself (a pre-blanked buffer would erase
71
+ * every specifier), so this is the figure it reports as its `blankedChars` —
72
+ * the number `wiring explain` / `gates explain` print as
73
+ * `scan: code (N chars blanked)`. Without it the note claimed a zone removed
74
+ * nothing while it had dropped a commented-out edge.
75
+ */
76
+ export declare function parseImportStatementsWithMeta(content: string, options?: IParseImportsOptions): {
77
+ readonly statements: IParsedImport[];
78
+ readonly blankedChars: number;
79
+ };
80
+ //# sourceMappingURL=parse-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-imports.d.ts","sourceRoot":"","sources":["../../src/extract/parse-imports.ts"],"names":[],"mappings":"AA8BA;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,KAAK,CAAC;AAE7C,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,sBAAsB;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;CACjC;AAED,kDAAkD;AAClD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAElE,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhG,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;CACpC;AAsID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,aAAa,EAAE,CAEjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC;IAAE,QAAQ,CAAC,UAAU,EAAE,aAAa,EAAE,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CA4EzE"}
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The ONE import-statement parser.
3
+ *
4
+ * Two features need to read import statements out of source text: the
5
+ * `--fix` planner (does this sink bind the token, and how?) and the
6
+ * `import-edges` extractor (which file imports what, from where?). A second
7
+ * parser would drift from the first, and the two would then disagree about
8
+ * what a file imports — which is precisely the class of silent disagreement
9
+ * this engine exists to eliminate. So both consume this.
10
+ *
11
+ * It is a lexer, not a compiler: it answers what the text says, which is the
12
+ * largest thing that stays honest across `.ts`, `.tsx`, `.mts` and plain JS
13
+ * with one code path.
14
+ *
15
+ * Round 11 (6.1a) made it the ONLY import extractor: `scanImports` (every
16
+ * boundary check, drift, impact, review packet…) and the `import-edges` DSL
17
+ * extractor both read through here, so "what does this file import" has one
18
+ * answer. By default it reads CODE only — a commented-out import, an import in
19
+ * a doc-comment code fence, or `"import x from 'y'"` inside a string is not a
20
+ * dependency. Measured against the TypeScript oracle over this repo (1716
21
+ * files): 79 phantom edges and 1 real miss before, 0 / 0 after.
22
+ */
23
+ import { blankZoneKinds, lexCodeZones, zoneContaining, zoneKeepsAt, } from "./code-zones.js";
24
+ /**
25
+ * Matches one import/re-export statement up to its specifier.
26
+ *
27
+ * The clause may span lines (a multi-line `{ … }` block is normal) but never
28
+ * contains a `;`, a quote or a BACKTICK, which is what keeps the lazy match
29
+ * from running past the end of its own statement into the next one. No real
30
+ * import clause holds a backtick; prose does — a doc comment's
31
+ * `` `shrk import <format> --populate` `` used to open a clause that ran on to
32
+ * the next real `from '…'`, so the raw reading (`--include-comments`,
33
+ * `scan: 'all'`) filed that import on the comment's line. The old scanImports
34
+ * regex excluded the backtick too; raw-mode lines match it again.
35
+ *
36
+ * `export … from '…'` is matched too: a re-export is an edge in the dependency
37
+ * graph exactly like an import, and a barrel that is invisible here would make
38
+ * every consumer behind it invisible as well.
39
+ *
40
+ * Linear by construction (round 11, 6.1(b)): the old form
41
+ * `\s+([^;'"]*?)\s*from` put three whitespace-consuming quantifiers side by
42
+ * side, which is fine on raw source and O(run²) once comments are blanked into
43
+ * long runs of spaces (6 / 24 / 94 / 371 ms as a JSDoc block doubled). Here a
44
+ * single `\s` separates the keyword from ONE lazy class, and `\bfrom` needs no
45
+ * whitespace quantifier in front of it, so each keyword costs one forward scan
46
+ * to its statement's end. Verified byte-identical to the old form on the raw
47
+ * text of every file in this repo. Do not reintroduce adjacent `\s*`/`\s+`
48
+ * around a lazy class — `r75-import-scan-zones.test.ts` times it.
49
+ */
50
+ const IMPORT_STATEMENT = /\b(import|export)\s([^;'"`]*?)\bfrom\s*['"]([^'"]+)['"]\s*;?/g;
51
+ /**
52
+ * The specifier-only forms, which bind no name: a side-effect import, a dynamic
53
+ * `import()`, and `require()`. Each is a real dependency edge — the boundary
54
+ * engine counts all three — even though none of them introduces a binding.
55
+ */
56
+ const SIDE_EFFECT_IMPORT = /\bimport\s*['"]([^'"]+)['"]\s*;?/g;
57
+ const DYNAMIC_IMPORT = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
58
+ const REQUIRE_CALL = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
59
+ /** Split a clause on commas that are OUTSIDE a `{ … }` block. */
60
+ function splitTopLevel(clause) {
61
+ const out = [];
62
+ let depth = 0;
63
+ let current = '';
64
+ for (const ch of clause) {
65
+ if (ch === '{')
66
+ depth += 1;
67
+ if (ch === '}')
68
+ depth -= 1;
69
+ if (ch === ',' && depth === 0) {
70
+ out.push(current);
71
+ current = '';
72
+ continue;
73
+ }
74
+ current += ch;
75
+ }
76
+ if (current.trim() !== '')
77
+ out.push(current);
78
+ return out;
79
+ }
80
+ /** Parse the clause between `import`/`export` and `from`. */
81
+ function parseClause(clause) {
82
+ const bindings = [];
83
+ for (const part of splitTopLevel(clause.trim())) {
84
+ const piece = part.trim();
85
+ if (piece === '')
86
+ continue;
87
+ if (piece.startsWith('*')) {
88
+ const m = /^\*\s*as\s+([A-Za-z_$][\w$]*)$/.exec(piece);
89
+ if (m)
90
+ bindings.push({ local: m[1], kind: 'namespace' });
91
+ continue;
92
+ }
93
+ if (piece.startsWith('{')) {
94
+ const inner = piece.replace(/^\{/, '').replace(/\}$/, '');
95
+ for (const spec of inner.split(',')) {
96
+ const s = spec.trim();
97
+ // An inline `type` specifier binds no runtime value.
98
+ if (s === '' || /^type\b/.test(s))
99
+ continue;
100
+ const aliased = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(s);
101
+ if (aliased) {
102
+ bindings.push({ local: aliased[2], kind: 'named', imported: aliased[1] });
103
+ continue;
104
+ }
105
+ if (/^[A-Za-z_$][\w$]*$/.test(s)) {
106
+ bindings.push({ local: s, kind: 'named', imported: s });
107
+ }
108
+ }
109
+ continue;
110
+ }
111
+ if (/^[A-Za-z_$][\w$]*$/.test(piece)) {
112
+ bindings.push({ local: piece, kind: 'default' });
113
+ }
114
+ }
115
+ return bindings;
116
+ }
117
+ /** Offsets of every `\n` in `content`, for O(log n) line lookups. */
118
+ function newlineOffsets(content) {
119
+ const out = [];
120
+ for (let i = content.indexOf('\n'); i !== -1; i = content.indexOf('\n', i + 1))
121
+ out.push(i);
122
+ return out;
123
+ }
124
+ /**
125
+ * 1-based line of `index`: one plus the number of newlines BEFORE it. The
126
+ * index is the keyword's own offset — the old `scanImports` regex consumed the
127
+ * preceding `\n` with `(?:^|\s)`, so every import after line 1 was reported
128
+ * one line early (round 11, 6.1a#line-numbers).
129
+ */
130
+ function lineAt(newlines, index) {
131
+ let lo = 0;
132
+ let hi = newlines.length;
133
+ while (lo < hi) {
134
+ const mid = (lo + hi) >> 1;
135
+ if (newlines[mid] < index)
136
+ lo = mid + 1;
137
+ else
138
+ hi = mid;
139
+ }
140
+ return lo + 1;
141
+ }
142
+ /** Zones blanked before matching in `code` mode: comments only — the specifier is a string. */
143
+ const COMMENT_ZONES = new Set(['comment']);
144
+ /**
145
+ * Offset (within the match) of the specifier's OPENING quote. Every pattern
146
+ * ends `<quote><specifier><quote>` followed only by whitespace, `;` or `)`,
147
+ * and no clause may contain a quote, so the last quote in the match closes the
148
+ * specifier.
149
+ */
150
+ function specifierQuoteOffset(match, specifier) {
151
+ const closing = Math.max(match.lastIndexOf("'"), match.lastIndexOf('"'));
152
+ return closing - specifier.length - 1;
153
+ }
154
+ /**
155
+ * Every import / re-export statement in `content`, with the names it binds.
156
+ *
157
+ * A `import type { … }` statement is returned with `typeOnly: true` rather than
158
+ * dropped: the `--fix` planner must ignore it (it binds no value), while the
159
+ * dependency graph legitimately counts it as an edge. Returning it and letting
160
+ * each caller decide keeps that judgement at the call site instead of baking
161
+ * one feature's answer into the shared parser.
162
+ *
163
+ * In the default `code` zone a statement is kept only when its keyword starts
164
+ * in a code zone AND its specifier quote opens a real string literal — the
165
+ * same {@link zoneKeepsAt} authority the policy plane and the extraction DSL
166
+ * use. Offsets, lines and `raw` always refer to the ORIGINAL text (blanking
167
+ * preserves length), so callers that splice `content` stay correct.
168
+ */
169
+ export function parseImportStatements(content, options = {}) {
170
+ return parseImportStatementsWithMeta(content, options).statements;
171
+ }
172
+ /**
173
+ * {@link parseImportStatements}, plus how many characters the zone BLANKED
174
+ * before matching: the comment characters under `code`, `0` under `all`.
175
+ *
176
+ * The `import-edges` extractor zones itself (a pre-blanked buffer would erase
177
+ * every specifier), so this is the figure it reports as its `blankedChars` —
178
+ * the number `wiring explain` / `gates explain` print as
179
+ * `scan: code (N chars blanked)`. Without it the note claimed a zone removed
180
+ * nothing while it had dropped a commented-out edge.
181
+ */
182
+ export function parseImportStatementsWithMeta(content, options = {}) {
183
+ const zone = options.zone ?? 'code';
184
+ const zones = zone === 'code' ? lexCodeZones(content) : undefined;
185
+ const blanked = zones ? blankZoneKinds(content, zones, COMMENT_ZONES) : undefined;
186
+ const text = blanked ? blanked.content : content;
187
+ // Built on first use: a file whose statements are all zoned out (or that has
188
+ // none) never needs a line table.
189
+ let newlines;
190
+ const lineOfIndex = (index) => lineAt((newlines ??= newlineOffsets(content)), index);
191
+ const keep = (index, match, specifier) => {
192
+ if (!zones)
193
+ return true;
194
+ const quote = index + specifierQuoteOffset(match, specifier);
195
+ // The specifier's quote must OPEN a plain string literal. "Inside a string"
196
+ // is not enough: `export const t = \`import z from 'w'\`` starts with a
197
+ // code `export`, and its `'w'` sits inside the template literal.
198
+ const specZone = zoneContaining(zones, quote);
199
+ return (zoneKeepsAt('code', zones, index) &&
200
+ zoneKeepsAt('strings', zones, quote) &&
201
+ specZone !== undefined &&
202
+ specZone.start === quote &&
203
+ specZone.template !== true);
204
+ };
205
+ const out = [];
206
+ const claimed = [];
207
+ let m;
208
+ IMPORT_STATEMENT.lastIndex = 0;
209
+ while ((m = IMPORT_STATEMENT.exec(text)) !== null) {
210
+ const specifier = m[3];
211
+ if (!keep(m.index, m[0], specifier))
212
+ continue;
213
+ const clause = m[2];
214
+ const typeOnly = /^type\b/.test(clause.trim());
215
+ claimed.push({ start: m.index, end: m.index + m[0].length });
216
+ out.push({
217
+ specifier,
218
+ bindings: typeOnly ? [] : parseClause(clause),
219
+ line: lineOfIndex(m.index),
220
+ index: m.index,
221
+ raw: content.slice(m.index, m.index + m[0].length),
222
+ typeOnly,
223
+ kind: m[1] === 'export' ? 'reexport' : 'import',
224
+ });
225
+ }
226
+ // A `import x from 'y'` also contains the text `import … 'y'`, so the
227
+ // specifier-only patterns would re-report it. Skipping anything inside an
228
+ // already-claimed span keeps each dependency counted exactly once.
229
+ const overlapsClaimed = (start) => claimed.some((c) => start >= c.start && start < c.end);
230
+ for (const [re, kind] of [
231
+ [SIDE_EFFECT_IMPORT, 'side-effect'],
232
+ [DYNAMIC_IMPORT, 'dynamic'],
233
+ [REQUIRE_CALL, 'require'],
234
+ ]) {
235
+ re.lastIndex = 0;
236
+ while ((m = re.exec(text)) !== null) {
237
+ if (overlapsClaimed(m.index))
238
+ continue;
239
+ const specifier = m[1];
240
+ if (!keep(m.index, m[0], specifier))
241
+ continue;
242
+ out.push({
243
+ specifier,
244
+ bindings: [],
245
+ line: lineOfIndex(m.index),
246
+ index: m.index,
247
+ raw: content.slice(m.index, m.index + m[0].length),
248
+ typeOnly: false,
249
+ kind,
250
+ });
251
+ }
252
+ }
253
+ return { statements: out.sort((a, b) => a.index - b.index), blankedChars: blanked?.blankedChars ?? 0 };
254
+ }