@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,22 @@
1
+ import type { UnitLivenessState } from '@shrkcrft/core';
2
+ import type { IBoundaryDeadUnit } from './evaluate-boundaries.js';
3
+ /**
4
+ * One settled selector unit of a boundary rule, as every boundary surface
5
+ * reports it (round 13): `check boundaries` text and `--json` (`intendedEmpty`,
6
+ * `wentLive`, `failingUnits`), MCP `check_boundaries`, the quality and finish
7
+ * notes. Built by `boundaryUnitFinding` from the one settle.
8
+ */
9
+ export interface IBoundaryUnitFinding {
10
+ readonly ruleId: string;
11
+ /** The boundary JSON's unit word: `from`, `forbidden`, `allowed` or `exemptFiles`. */
12
+ readonly unit: IBoundaryDeadUnit['unit'];
13
+ readonly selector: string;
14
+ readonly state: UnitLivenessState;
15
+ /** THE sentence for the unit (`formatUnitLiveness`), without the selector the row already names. */
16
+ readonly reason: string;
17
+ /** The marker's own reason, when the unit is marked `expectEmpty`. */
18
+ readonly markReason?: string;
19
+ /** The pack that stamped the marker: a pack marker that went live is INFO and never fails the run. */
20
+ readonly packageName?: string;
21
+ }
22
+ //# sourceMappingURL=i-boundary-unit-finding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i-boundary-unit-finding.d.ts","sourceRoot":"","sources":["../../src/evaluate/i-boundary-unit-finding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,oGAAoG;IACpG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,sGAAsG;IACtG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { IBoundaryRuleSettleInput } from './i-boundary-rule-settle-input.js';
2
+ import type { IBoundaryRuleSettlement } from './i-boundary-rule-settlement.js';
3
+ /**
4
+ * THE settle of one boundary rule (round 13). The evaluator calls it with what
5
+ * it observed; the orchestrator's unread re-settle (`boundaryUnitLiveness` —
6
+ * `check boundaries` AND `--diff-against`) calls it with the claims an unread
7
+ * file could refute withdrawn. One function, so the two paths can never settle
8
+ * one rule differently:
9
+ *
10
+ * - units through core's `settleUnitLiveness`: a `from` inclusion is
11
+ * Coverage weight (dead → a shortfall, exit 2), every other list is
12
+ * Advisory (listed, ✓ withheld); a marked unit whose target does not
13
+ * exist is intended-empty (record B, printed `accepted by expectEmpty`),
14
+ * one whose target exists went live;
15
+ * - the rule's emptiness through `settleRuleEmptiness`: a rule whose every
16
+ * `from` inclusion is intended-empty and that matched no file is
17
+ * IntendedEmpty — accepted, never failOnEmpty; an unread governed file
18
+ * makes it partial, never "matched nothing";
19
+ * - the dead list is the settled `.dead` (unmarked units only), each worded
20
+ * by the one per-unit line.
21
+ */
22
+ export declare function settleBoundaryRule(input: IBoundaryRuleSettleInput): IBoundaryRuleSettlement;
23
+ //# sourceMappingURL=settle-boundary-rule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle-boundary-rule.d.ts","sourceRoot":"","sources":["../../src/evaluate/settle-boundary-rule.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE/E;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,wBAAwB,GAAG,uBAAuB,CAiD3F"}
@@ -0,0 +1,73 @@
1
+ import { settleRuleEmptiness, settleUnitLiveness, UnitDeadWeight, unitStateLists, } from '@shrkcrft/core';
2
+ import { BoundaryMarkableList } from "../model/boundary-markable-list.js";
3
+ import { boundaryRuleFailsOnEmpty } from "../model/boundary-rule-scope.js";
4
+ import { boundaryUnitFinding } from "./boundary-unit-finding.js";
5
+ /**
6
+ * THE settle of one boundary rule (round 13). The evaluator calls it with what
7
+ * it observed; the orchestrator's unread re-settle (`boundaryUnitLiveness` —
8
+ * `check boundaries` AND `--diff-against`) calls it with the claims an unread
9
+ * file could refute withdrawn. One function, so the two paths can never settle
10
+ * one rule differently:
11
+ *
12
+ * - units through core's `settleUnitLiveness`: a `from` inclusion is
13
+ * Coverage weight (dead → a shortfall, exit 2), every other list is
14
+ * Advisory (listed, ✓ withheld); a marked unit whose target does not
15
+ * exist is intended-empty (record B, printed `accepted by expectEmpty`),
16
+ * one whose target exists went live;
17
+ * - the rule's emptiness through `settleRuleEmptiness`: a rule whose every
18
+ * `from` inclusion is intended-empty and that matched no file is
19
+ * IntendedEmpty — accepted, never failOnEmpty; an unread governed file
20
+ * makes it partial, never "matched nothing";
21
+ * - the dead list is the settled `.dead` (unmarked units only), each worded
22
+ * by the one per-unit line.
23
+ */
24
+ export function settleBoundaryRule(input) {
25
+ const universeNote = input.fileUniverse === 'edges' ? ' (file list derived from import edges)' : '';
26
+ const settled = settleUnitLiveness({
27
+ unitLabel: 'scope globs',
28
+ acceptanceUnitLabel: 'selector units',
29
+ weight: UnitDeadWeight.Advisory,
30
+ observations: input.observations,
31
+ marks: input.rule.expectEmptyUnits ?? [],
32
+ deadSummary: `reached no governed file among ${input.filesScanned} scanned${universeNote}`,
33
+ });
34
+ const noFiles = `from globs matched 0 of ${input.filesScanned} scanned files: ${input.includeGlobs.join(', ')}`;
35
+ const emptiness = settleRuleEmptiness({
36
+ unitLabel: 'governed files',
37
+ filesMatched: input.filesInScope + input.exemptFilesInScope,
38
+ unitsMatched: input.filesInScope,
39
+ unread: input.unread,
40
+ emptiedByNegations: input.filesInScope === 0 && input.exemptFilesInScope > 0,
41
+ emptiedReason: `every file its from globs match is exempt (${input.exemptFilesInScope} file(s))`,
42
+ liveness: settled,
43
+ primaryLists: [BoundaryMarkableList.From],
44
+ failOnEmpty: boundaryRuleFailsOnEmpty(input.rule),
45
+ noFilesReason: noFiles,
46
+ noUnitsReason: noFiles,
47
+ });
48
+ const status = emptiness.skipped ? 'skipped' : input.violations > 0 ? 'failed' : 'passed';
49
+ const coverage = emptiness.coverage ??
50
+ settled.shortfall ?? { unit: 'scope globs', expected: 0, examined: 0 };
51
+ const deadUnits = settled.dead.map((u) => {
52
+ const f = boundaryUnitFinding(input.rule.id, u);
53
+ return {
54
+ ruleId: f.ruleId,
55
+ unit: f.unit,
56
+ selector: f.selector,
57
+ reason: f.reason,
58
+ ...(u.cause !== undefined ? { cause: u.cause } : {}),
59
+ };
60
+ });
61
+ const hasUnits = settled.dead.length + settled.intendedEmpty.length + settled.wentLive.length > 0;
62
+ return {
63
+ settled,
64
+ emptiness,
65
+ status,
66
+ ...(emptiness.skipReason !== undefined ? { skipReason: emptiness.skipReason } : {}),
67
+ failedOnEmpty: emptiness.fails,
68
+ coverage,
69
+ ...(settled.acceptance !== undefined ? { unitAcceptance: settled.acceptance } : {}),
70
+ ...(hasUnits ? { units: unitStateLists(settled) } : {}),
71
+ deadUnits,
72
+ };
73
+ }
@@ -0,0 +1,11 @@
1
+ import type { IBoundaryRuleCoverage } from './evaluate-boundaries.js';
2
+ import type { IBoundaryRuleSettlement } from './i-boundary-rule-settlement.js';
3
+ /**
4
+ * A rule's coverage record with a settlement applied (round 13): its status,
5
+ * skip, verdict coverage, acceptance, dead list, settled units and every row's
6
+ * `state`. The evaluator and the orchestrator's unread re-settle both build the
7
+ * record here, so neither can carry a field the other drops: a skip reason or
8
+ * an acceptance the new settlement does not carry is gone, never inherited.
9
+ */
10
+ export declare function withBoundaryRuleSettlement(counts: Pick<IBoundaryRuleCoverage, 'ruleId' | 'severity' | 'filesInScope' | 'exemptFilesInScope' | 'edgesInScope' | 'violations' | 'suppressed' | 'fromGlobs' | 'forbidden' | 'allowed' | 'exemptions' | 'exceptions'>, s: IBoundaryRuleSettlement): IBoundaryRuleCoverage;
11
+ //# sourceMappingURL=with-boundary-rule-settlement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with-boundary-rule-settlement.d.ts","sourceRoot":"","sources":["../../src/evaluate/with-boundary-rule-settlement.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE/E;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,IAAI,CACV,qBAAqB,EACnB,QAAQ,GACR,UAAU,GACV,cAAc,GACd,oBAAoB,GACpB,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,WAAW,GACX,WAAW,GACX,SAAS,GACT,YAAY,GACZ,YAAY,CACf,EACD,CAAC,EAAE,uBAAuB,GACzB,qBAAqB,CAoCvB"}
@@ -0,0 +1,44 @@
1
+ import { RuleEmptiness } from '@shrkcrft/core';
2
+ import { BoundaryMarkableList } from "../model/boundary-markable-list.js";
3
+ /**
4
+ * A rule's coverage record with a settlement applied (round 13): its status,
5
+ * skip, verdict coverage, acceptance, dead list, settled units and every row's
6
+ * `state`. The evaluator and the orchestrator's unread re-settle both build the
7
+ * record here, so neither can carry a field the other drops: a skip reason or
8
+ * an acceptance the new settlement does not carry is gone, never inherited.
9
+ */
10
+ export function withBoundaryRuleSettlement(counts, s) {
11
+ const stateOf = new Map(s.settled.units.map((u) => [`${u.list} ${u.unit}`, u.state]));
12
+ const withState = (row, list, unit) => {
13
+ const { state: _previous, ...bare } = row;
14
+ const state = stateOf.get(`${list} ${unit}`);
15
+ return (state !== undefined ? { ...bare, state } : bare);
16
+ };
17
+ return {
18
+ ruleId: counts.ruleId,
19
+ severity: counts.severity,
20
+ status: s.status,
21
+ ...(s.skipReason !== undefined ? { skipReason: s.skipReason } : {}),
22
+ ...(s.failedOnEmpty ? { failedOnEmpty: true } : {}),
23
+ ...(s.emptiness.state === RuleEmptiness.IntendedEmpty ? { acceptedAsIntendedEmpty: true } : {}),
24
+ filesInScope: counts.filesInScope,
25
+ exemptFilesInScope: counts.exemptFilesInScope,
26
+ edgesInScope: counts.edgesInScope,
27
+ violations: counts.violations,
28
+ suppressed: counts.suppressed,
29
+ fromGlobs: counts.fromGlobs.map((g) => withState(g, BoundaryMarkableList.From, g.glob)),
30
+ forbidden: counts.forbidden.map((f) => withState(f, BoundaryMarkableList.ForbiddenImports, f.pattern)),
31
+ allowed: counts.allowed.map((a) => withState(a, BoundaryMarkableList.AllowedImports, a.pattern)),
32
+ exemptions: counts.exemptions.map((e) => e.origin === 'from-negation'
33
+ ? withState(e, BoundaryMarkableList.From, `!${e.glob}`)
34
+ : e.origin === 'exemptFiles'
35
+ ? withState(e, BoundaryMarkableList.ExemptFiles, e.glob)
36
+ : e),
37
+ exceptions: counts.exceptions,
38
+ deadUnits: s.deadUnits,
39
+ unitLiveness: s.settled.units,
40
+ coverage: s.coverage,
41
+ ...(s.unitAcceptance !== undefined ? { unitAcceptance: s.unitAcceptance } : {}),
42
+ ...(s.units !== undefined ? { units: s.units } : {}),
43
+ };
44
+ }
@@ -1,3 +1,4 @@
1
+ import type { ScanZone } from '@shrkcrft/core';
1
2
  /**
2
3
  * Which lexical zone of a file a character belongs to.
3
4
  *
@@ -14,14 +15,49 @@ export interface ICodeZone {
14
15
  readonly kind: CodeZoneKind;
15
16
  readonly start: number;
16
17
  readonly end: number;
18
+ /**
19
+ * Set on a `string` zone delimited by BACKTICKS.
20
+ *
21
+ * A template literal is the one string form that routinely carries real
22
+ * content a rule must see — an inline `template:`, a SQL/GraphQL tagged
23
+ * query. Tagging it lets `code-and-templates` keep those bodies while still
24
+ * blanking comments and plain quoted strings, without a second lexer.
25
+ */
26
+ readonly template?: boolean;
27
+ /**
28
+ * Set on a `code` zone that is one REGEX LITERAL (`/…/flags`).
29
+ *
30
+ * A regex literal is code, but its body is opaque: the backtick in
31
+ * `` /`/g `` must not open a template literal and the `/*` in `/\/*$/` must
32
+ * not open a block comment. Either mis-lex used to swallow every real
33
+ * construct up to the next backtick / `*\/` in the file — a real `import()`
34
+ * after it vanished from the code zone, and every boundary surface passed
35
+ * over a forbidden import (round 11 review).
36
+ */
37
+ readonly regex?: boolean;
17
38
  }
39
+ /** Run `fn` with the `lexCodeZones` memo enabled (nestable), then clear it. */
40
+ export declare function withLexCache<T>(fn: () => T): T;
41
+ /** How many contents were actually lexed, and how many lexes the memo served (tests / perf locks). */
42
+ export declare function lexCodeZonesStats(): {
43
+ readonly lexed: number;
44
+ readonly memoHits: number;
45
+ };
46
+ /** Reset {@link lexCodeZonesStats}. */
47
+ export declare function resetLexCodeZonesStats(): void;
18
48
  /**
19
49
  * Split `content` into contiguous code / string / comment zones.
20
50
  *
21
51
  * A lexer, not a parser: it recognises the C/JS-family string quotes
22
- * (`'`, `"`, `` ` ``) and comment forms (`//`, `/* … *\/`). Documented limits
23
- * a regex literal containing `//` reads as a comment, and `#`-comment languages
24
- * are reported as code.
52
+ * (`'`, `"`, `` ` ``), comment forms (`//`, `/* … *\/`) and regex literals.
53
+ * A `/` in expression position opens a regex literal (a `code` zone tagged
54
+ * `regex`, see {@link ICodeZone.regex}) whose quotes, backticks and `/*` open
55
+ * nothing. Documented limits — `#`-comment languages are reported as code, and
56
+ * a regex right after `)` (`if (x) /re/`) is read as a division, i.e. with the
57
+ * pre-round-11 lexing of its body.
58
+ *
59
+ * Inside {@link withLexCache} the result is memoised by content and shared
60
+ * (frozen) between callers.
25
61
  */
26
62
  export declare function lexCodeZones(content: string): readonly ICodeZone[];
27
63
  /**
@@ -29,4 +65,60 @@ export declare function lexCodeZones(content: string): readonly ICodeZone[];
29
65
  * recorded zone (e.g. past end-of-content).
30
66
  */
31
67
  export declare function zoneAt(zones: readonly ICodeZone[], index: number): CodeZoneKind;
68
+ /**
69
+ * The lexed zone containing `index` (`undefined` past the end) — for a caller
70
+ * that needs a zone's BOUNDS, not just its kind: the import parser requires a
71
+ * specifier's quote to OPEN a string, which "is inside a string" cannot tell
72
+ * apart from a quote sitting inside a template literal.
73
+ */
74
+ export declare function zoneContaining(zones: readonly ICodeZone[], index: number): ICodeZone | undefined;
75
+ /**
76
+ * Does `zone` keep a match starting at `index`?
77
+ *
78
+ * The ONE place that maps a {@link ScanZone} onto a lexed offset. Both raw-text
79
+ * engines call it — the policy plane per finding, the extraction DSL through
80
+ * {@link blankOutsideZone} — so "what does `code` mean" cannot drift between
81
+ * the two planes an author moves a rule between.
82
+ *
83
+ * Narrower zones require the match to START in the named zone: a pattern
84
+ * spanning out of a comment is judged by where it began, which is the only
85
+ * offset a regex engine reports.
86
+ */
87
+ export declare function zoneKeepsAt(zone: ScanZone, zones: readonly ICodeZone[], index: number): boolean;
88
+ /** A run of characters {@link blankOutsideZone} replaced with whitespace. */
89
+ export interface IBlankedSpan {
90
+ readonly kind: CodeZoneKind;
91
+ readonly start: number;
92
+ readonly end: number;
93
+ }
94
+ /** What {@link blankOutsideZone} produced, plus what it had to remove to get there. */
95
+ export interface IBlankedContent {
96
+ /** Same LENGTH as the input — every dropped character became a space or newline. */
97
+ readonly content: string;
98
+ readonly blankedSpans: readonly IBlankedSpan[];
99
+ readonly blankedChars: number;
100
+ }
101
+ /**
102
+ * Return `content` with every span OUTSIDE `zone` replaced by equal-length
103
+ * whitespace.
104
+ *
105
+ * Blanking rather than deleting is the load-bearing detail: the result has the
106
+ * same length and the same newlines as the input, so every offset and every
107
+ * line number a downstream extractor reports is still the real one in the real
108
+ * file. That is what lets a zone apply to EVERY extractor kind — `array-members`,
109
+ * `call-args`, `object-keys` — instead of only the regex form, for free and with
110
+ * no per-extractor zone logic to keep in sync.
111
+ */
112
+ export declare function blankOutsideZone(content: string, zone: ScanZone): IBlankedContent;
113
+ /**
114
+ * Blank every lexed zone whose KIND is in `kinds`, keeping everything else.
115
+ *
116
+ * The import extractor needs the one shape no {@link ScanZone} expresses:
117
+ * comments gone (so a commented-out import is not an edge, and an apostrophe
118
+ * in a comment cannot cut a real import clause short) while string literals
119
+ * stay (the specifier IS a string). `zones` is passed in so a caller that also
120
+ * judges offsets against the lex does not lex twice. Same blanking loop as
121
+ * {@link blankOutsideZone} — one implementation of "blank a zone".
122
+ */
123
+ export declare function blankZoneKinds(content: string, zones: readonly ICodeZone[], kinds: ReadonlySet<CodeZoneKind>): IBlankedContent;
32
124
  //# sourceMappingURL=code-zones.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"code-zones.d.ts","sourceRoot":"","sources":["../../src/extract/code-zones.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CA4BlE;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAY/E"}
1
+ {"version":3,"file":"code-zones.d.ts","sourceRoot":"","sources":["../../src/extract/code-zones.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AA6HD,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQ9C;AAED,sGAAsG;AACtG,wBAAgB,iBAAiB,IAAI;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAEzF;AAED,uCAAuC;AACvC,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CAiBlE;AAgDD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAY/E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAWhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,KAAK,EAAE,MAAM,GACZ,OAAO,CAsBT;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,eAAe,CAIjF;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,KAAK,EAAE,WAAW,CAAC,YAAY,CAAC,GAC/B,eAAe,CAEjB"}
@@ -1,13 +1,187 @@
1
1
  import { skipComment, skipString } from "./scan-literals.js";
2
+ /** Characters after which a `/` starts an EXPRESSION (a regex literal), never a division. */
3
+ const REGEX_AFTER_PUNCTUATION = new Set([
4
+ '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';',
5
+ '+', '-', '*', '%', '<', '>', '~', '^', '/',
6
+ ]);
7
+ /** Keywords after which a `/` starts a regex literal (`return /x/.test(s)`). */
8
+ const REGEX_AFTER_KEYWORD = new Set([
9
+ 'return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'throw',
10
+ 'yield', 'await', 'instanceof', 'else', 'do',
11
+ ]);
12
+ const isIdentifierChar = (c) => c !== undefined && /[A-Za-z0-9_$]/.test(c);
13
+ const isBlank = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === '\v';
14
+ /**
15
+ * Is a `/` whose previous significant code character sits at `prev` in
16
+ * EXPRESSION position — i.e. does it open a regex literal rather than divide?
17
+ * `prev < 0` is the start of the file. After an operand (an identifier, a
18
+ * number, `)` or `]`) it is a division; after an operator, an opening bracket
19
+ * or one of {@link REGEX_AFTER_KEYWORD} it is a regex. `)` is judged a division
20
+ * on purpose: `(a + b) / 2` is far more common than `if (x) /re/.test(y)`, and
21
+ * a missed regex only falls back to the pre-round-11 reading.
22
+ */
23
+ function regexMayStartAfter(content, prev) {
24
+ if (prev < 0)
25
+ return true;
26
+ const ch = content[prev];
27
+ if (REGEX_AFTER_PUNCTUATION.has(ch))
28
+ return true;
29
+ if (!isIdentifierChar(ch))
30
+ return false;
31
+ let start = prev;
32
+ while (start > 0 && isIdentifierChar(content[start - 1]))
33
+ start -= 1;
34
+ // `obj.return / 2` — a property named like a keyword is an operand.
35
+ if (start > 0 && content[start - 1] === '.')
36
+ return false;
37
+ return REGEX_AFTER_KEYWORD.has(content.slice(start, prev + 1));
38
+ }
39
+ /**
40
+ * If a regex literal starts at `start` (a `/` already judged to be in
41
+ * expression position, not followed by `/` or `*`), the index of its LAST
42
+ * character (the closing `/`, or its last flag); otherwise `-1`.
43
+ *
44
+ * Bounded to the line — a regex literal cannot contain a line terminator — so
45
+ * a `/` misjudged as a regex start costs at most the rest of one line, and a
46
+ * lone division with no closing `/` on its line is not a regex at all. A `/`
47
+ * inside a `[...]` class does not close the literal; `\` escapes one character.
48
+ */
49
+ function skipRegexLiteral(content, start) {
50
+ let inClass = false;
51
+ for (let i = start + 1; i < content.length; i += 1) {
52
+ const c = content[i];
53
+ if (c === '\n' || c === '\r')
54
+ return -1;
55
+ if (c === '\\') {
56
+ const next = content[i + 1];
57
+ if (next === undefined || next === '\n' || next === '\r')
58
+ return -1;
59
+ i += 1;
60
+ continue;
61
+ }
62
+ if (inClass) {
63
+ if (c === ']')
64
+ inClass = false;
65
+ continue;
66
+ }
67
+ if (c === '[') {
68
+ inClass = true;
69
+ continue;
70
+ }
71
+ if (c === '/') {
72
+ let end = i;
73
+ while (end + 1 < content.length && /[A-Za-z]/.test(content[end + 1]))
74
+ end += 1;
75
+ return end;
76
+ }
77
+ }
78
+ return -1;
79
+ }
80
+ /**
81
+ * Does the `/` at `i` open a regex literal? Judged LAZILY, only for the rare
82
+ * `/` that is not a comment: scan back over blanks to the previous significant
83
+ * code character. The pending code region is `[codeStart, i)`; before it, the
84
+ * recorded zones partition `[0, codeStart)` — a comment is trivia (keep
85
+ * scanning before it), a string or regex literal is an operand (so the `/`
86
+ * divides), a code zone is scanned like the pending region, and running out of
87
+ * zones is the start of the file.
88
+ *
89
+ * This is exactly the previous-significant-character judgement the lexer used
90
+ * to track EAGERLY on every character (round 11 review: that bookkeeping made
91
+ * `lexCodeZones` ~1.8x slower under every zoned plane and the import parser).
92
+ */
93
+ function slashOpensRegex(content, zones, codeStart, i) {
94
+ let lo = codeStart;
95
+ let j = i - 1;
96
+ let z = zones.length - 1;
97
+ for (;;) {
98
+ while (j >= lo && isBlank(content[j]))
99
+ j -= 1;
100
+ if (j >= lo)
101
+ return regexMayStartAfter(content, j);
102
+ if (z < 0)
103
+ return true;
104
+ const zone = zones[z];
105
+ z -= 1;
106
+ if (zone.kind === 'comment') {
107
+ lo = zone.start;
108
+ j = zone.start - 1;
109
+ continue;
110
+ }
111
+ if (zone.kind === 'string' || zone.regex === true)
112
+ return false;
113
+ lo = zone.start;
114
+ j = zone.end - 1;
115
+ }
116
+ }
117
+ /**
118
+ * Memo of `lexCodeZones`, keyed by the CONTENT itself — so an entry can never
119
+ * be stale (the key IS the bytes), even across a spawn or a write. Enabled only
120
+ * inside {@link withLexCache}: one `shrk quality` run lexes the same file under
121
+ * the plane check, the coverage pass and every zoned rule of both; within a
122
+ * window each distinct content is lexed once. Bounded, and cleared when the
123
+ * outermost window closes.
124
+ */
125
+ const LEX_MEMO = new Map();
126
+ const LEX_MEMO_LIMIT = 50_000;
127
+ let lexWindowDepth = 0;
128
+ let lexCount = 0;
129
+ let lexMemoHits = 0;
130
+ /** Run `fn` with the `lexCodeZones` memo enabled (nestable), then clear it. */
131
+ export function withLexCache(fn) {
132
+ lexWindowDepth += 1;
133
+ try {
134
+ return fn();
135
+ }
136
+ finally {
137
+ lexWindowDepth -= 1;
138
+ if (lexWindowDepth === 0)
139
+ LEX_MEMO.clear();
140
+ }
141
+ }
142
+ /** How many contents were actually lexed, and how many lexes the memo served (tests / perf locks). */
143
+ export function lexCodeZonesStats() {
144
+ return { lexed: lexCount, memoHits: lexMemoHits };
145
+ }
146
+ /** Reset {@link lexCodeZonesStats}. */
147
+ export function resetLexCodeZonesStats() {
148
+ lexCount = 0;
149
+ lexMemoHits = 0;
150
+ }
2
151
  /**
3
152
  * Split `content` into contiguous code / string / comment zones.
4
153
  *
5
154
  * A lexer, not a parser: it recognises the C/JS-family string quotes
6
- * (`'`, `"`, `` ` ``) and comment forms (`//`, `/* … *\/`). Documented limits
7
- * a regex literal containing `//` reads as a comment, and `#`-comment languages
8
- * are reported as code.
155
+ * (`'`, `"`, `` ` ``), comment forms (`//`, `/* … *\/`) and regex literals.
156
+ * A `/` in expression position opens a regex literal (a `code` zone tagged
157
+ * `regex`, see {@link ICodeZone.regex}) whose quotes, backticks and `/*` open
158
+ * nothing. Documented limits — `#`-comment languages are reported as code, and
159
+ * a regex right after `)` (`if (x) /re/`) is read as a division, i.e. with the
160
+ * pre-round-11 lexing of its body.
161
+ *
162
+ * Inside {@link withLexCache} the result is memoised by content and shared
163
+ * (frozen) between callers.
9
164
  */
10
165
  export function lexCodeZones(content) {
166
+ if (lexWindowDepth > 0) {
167
+ const hit = LEX_MEMO.get(content);
168
+ if (hit !== undefined) {
169
+ lexMemoHits += 1;
170
+ return hit;
171
+ }
172
+ }
173
+ const zones = lexUncached(content);
174
+ lexCount += 1;
175
+ if (lexWindowDepth > 0) {
176
+ if (LEX_MEMO.size >= LEX_MEMO_LIMIT)
177
+ LEX_MEMO.clear();
178
+ const frozen = Object.freeze(zones);
179
+ LEX_MEMO.set(content, frozen);
180
+ return frozen;
181
+ }
182
+ return zones;
183
+ }
184
+ function lexUncached(content) {
11
185
  const zones = [];
12
186
  let codeStart = 0;
13
187
  const flushCode = (upTo) => {
@@ -19,7 +193,12 @@ export function lexCodeZones(content) {
19
193
  if (c === '"' || c === "'" || c === '`') {
20
194
  flushCode(i);
21
195
  const end = skipString(content, i);
22
- zones.push({ kind: 'string', start: i, end: end + 1 });
196
+ zones.push({
197
+ kind: 'string',
198
+ start: i,
199
+ end: end + 1,
200
+ ...(c === '`' ? { template: true } : {}),
201
+ });
23
202
  i = end;
24
203
  codeStart = i + 1;
25
204
  continue;
@@ -31,6 +210,17 @@ export function lexCodeZones(content) {
31
210
  zones.push({ kind: 'comment', start: i, end: end + 1 });
32
211
  i = end;
33
212
  codeStart = i + 1;
213
+ continue;
214
+ }
215
+ if (slashOpensRegex(content, zones, codeStart, i)) {
216
+ const reEnd = skipRegexLiteral(content, i);
217
+ if (reEnd >= 0) {
218
+ flushCode(i);
219
+ zones.push({ kind: 'code', start: i, end: reEnd + 1, regex: true });
220
+ i = reEnd;
221
+ codeStart = i + 1;
222
+ continue;
223
+ }
34
224
  }
35
225
  }
36
226
  }
@@ -57,3 +247,122 @@ export function zoneAt(zones, index) {
57
247
  }
58
248
  return 'code';
59
249
  }
250
+ /**
251
+ * The lexed zone containing `index` (`undefined` past the end) — for a caller
252
+ * that needs a zone's BOUNDS, not just its kind: the import parser requires a
253
+ * specifier's quote to OPEN a string, which "is inside a string" cannot tell
254
+ * apart from a quote sitting inside a template literal.
255
+ */
256
+ export function zoneContaining(zones, index) {
257
+ let lo = 0;
258
+ let hi = zones.length - 1;
259
+ while (lo <= hi) {
260
+ const mid = (lo + hi) >> 1;
261
+ const z = zones[mid];
262
+ if (index < z.start)
263
+ hi = mid - 1;
264
+ else if (index >= z.end)
265
+ lo = mid + 1;
266
+ else
267
+ return z;
268
+ }
269
+ return undefined;
270
+ }
271
+ /**
272
+ * Does `zone` keep a match starting at `index`?
273
+ *
274
+ * The ONE place that maps a {@link ScanZone} onto a lexed offset. Both raw-text
275
+ * engines call it — the policy plane per finding, the extraction DSL through
276
+ * {@link blankOutsideZone} — so "what does `code` mean" cannot drift between
277
+ * the two planes an author moves a rule between.
278
+ *
279
+ * Narrower zones require the match to START in the named zone: a pattern
280
+ * spanning out of a comment is judged by where it began, which is the only
281
+ * offset a regex engine reports.
282
+ */
283
+ export function zoneKeepsAt(zone, zones, index) {
284
+ if (zone === 'all')
285
+ return true;
286
+ let lo = 0;
287
+ let hi = zones.length - 1;
288
+ let hit;
289
+ while (lo <= hi) {
290
+ const mid = (lo + hi) >> 1;
291
+ const z = zones[mid];
292
+ if (index < z.start)
293
+ hi = mid - 1;
294
+ else if (index >= z.end)
295
+ lo = mid + 1;
296
+ else {
297
+ hit = z;
298
+ break;
299
+ }
300
+ }
301
+ // Outside every recorded zone (e.g. past end-of-content) reads as code.
302
+ const kind = hit?.kind ?? 'code';
303
+ if (zone === 'code')
304
+ return kind === 'code';
305
+ if (zone === 'strings')
306
+ return kind === 'string';
307
+ if (zone === 'comments')
308
+ return kind === 'comment';
309
+ // code-and-templates
310
+ return kind === 'code' || (kind === 'string' && hit?.template === true);
311
+ }
312
+ /**
313
+ * Return `content` with every span OUTSIDE `zone` replaced by equal-length
314
+ * whitespace.
315
+ *
316
+ * Blanking rather than deleting is the load-bearing detail: the result has the
317
+ * same length and the same newlines as the input, so every offset and every
318
+ * line number a downstream extractor reports is still the real one in the real
319
+ * file. That is what lets a zone apply to EVERY extractor kind — `array-members`,
320
+ * `call-args`, `object-keys` — instead of only the regex form, for free and with
321
+ * no per-extractor zone logic to keep in sync.
322
+ */
323
+ export function blankOutsideZone(content, zone) {
324
+ if (zone === 'all')
325
+ return { content, blankedSpans: [], blankedChars: 0 };
326
+ const zones = lexCodeZones(content);
327
+ return blankZonesWhere(content, zones, (z) => !zoneKeepsAt(zone, zones, z.start));
328
+ }
329
+ /**
330
+ * Blank every lexed zone whose KIND is in `kinds`, keeping everything else.
331
+ *
332
+ * The import extractor needs the one shape no {@link ScanZone} expresses:
333
+ * comments gone (so a commented-out import is not an edge, and an apostrophe
334
+ * in a comment cannot cut a real import clause short) while string literals
335
+ * stay (the specifier IS a string). `zones` is passed in so a caller that also
336
+ * judges offsets against the lex does not lex twice. Same blanking loop as
337
+ * {@link blankOutsideZone} — one implementation of "blank a zone".
338
+ */
339
+ export function blankZoneKinds(content, zones, kinds) {
340
+ return blankZonesWhere(content, zones, (z) => kinds.has(z.kind));
341
+ }
342
+ /**
343
+ * The ONE blanking loop: replace each selected zone with equal-length
344
+ * whitespace. Newlines survive so line numbers — and any `m`-flag anchor —
345
+ * hold. Built from slices (not a per-character array) so a large file costs
346
+ * one pass.
347
+ */
348
+ function blankZonesWhere(content, zones, blank) {
349
+ const parts = [];
350
+ const blankedSpans = [];
351
+ let blankedChars = 0;
352
+ let cursor = 0;
353
+ for (const z of zones) {
354
+ if (!blank(z))
355
+ continue;
356
+ const start = Math.min(z.start, content.length);
357
+ const end = Math.min(z.end, content.length);
358
+ if (start > cursor)
359
+ parts.push(content.slice(cursor, start));
360
+ parts.push(content.slice(start, end).replace(/[^\n]/g, ' '));
361
+ cursor = end;
362
+ blankedSpans.push({ kind: z.kind, start: z.start, end: z.end });
363
+ blankedChars += z.end - z.start;
364
+ }
365
+ if (cursor < content.length)
366
+ parts.push(content.slice(cursor));
367
+ return { content: parts.join(''), blankedSpans, blankedChars };
368
+ }