@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,14 @@
1
+ import type { IUnreadFile } from './unread-file.js';
2
+ /**
3
+ * What one rule's walk covered: the glob-matched files in its scope that were
4
+ * READ, and the ones that were not. `readScopeCoverage` folds this into the
5
+ * rule's coverage record. It is the only function that decides how an unread
6
+ * file changes a verdict.
7
+ */
8
+ export interface IReadScope {
9
+ /** Glob-matched files in the rule's scope that the reader read. */
10
+ readonly read: number;
11
+ /** Glob-matched files in the rule's scope that it did not read. */
12
+ readonly unread: readonly IUnreadFile[];
13
+ }
14
+ //# sourceMappingURL=read-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-scope.d.ts","sourceRoot":"","sources":["../../src/util/read-scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { IMatchedFiles } from './matched-files.js';
2
+ /**
3
+ * THE reader for ONE glob list: the one reader's positive walk
4
+ * (`readMatchingFiles`, memoised), narrowed to the files the list SELECTS
5
+ * (`globListSelects` — an inclusion glob matches, no negation does) and to the
6
+ * unread entries in front of it (`unreadMatching`, negation-aware).
7
+ *
8
+ * Every single-list plane reader goes through here — an extraction source, an
9
+ * extractor baseline, a doc-reference rule, a generated tree, a trace — so "which
10
+ * files does this list read?" has one answer, and an over-cap file the author
11
+ * excluded never makes the rule PARTIAL. A caller walking the UNION of several
12
+ * lists must not use this on the union (one list's `!` would leak into
13
+ * another's scope): walk the union, then select per list.
14
+ */
15
+ export declare function readSelectedFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): IMatchedFiles;
16
+ //# sourceMappingURL=read-selected-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-selected-files.d.ts","sourceRoot":"","sources":["../../src/util/read-selected-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,aAAa,CAOf"}
@@ -0,0 +1,25 @@
1
+ import { globListSelects } from "../scan/glob.js";
2
+ import { unreadMatching } from "./read-scope-coverage.js";
3
+ import { readMatchingFiles } from "./walk-files.js";
4
+ /**
5
+ * THE reader for ONE glob list: the one reader's positive walk
6
+ * (`readMatchingFiles`, memoised), narrowed to the files the list SELECTS
7
+ * (`globListSelects` — an inclusion glob matches, no negation does) and to the
8
+ * unread entries in front of it (`unreadMatching`, negation-aware).
9
+ *
10
+ * Every single-list plane reader goes through here — an extraction source, an
11
+ * extractor baseline, a doc-reference rule, a generated tree, a trace — so "which
12
+ * files does this list read?" has one answer, and an over-cap file the author
13
+ * excluded never makes the rule PARTIAL. A caller walking the UNION of several
14
+ * lists must not use this on the union (one list's `!` would leak into
15
+ * another's scope): walk the union, then select per list.
16
+ */
17
+ export function readSelectedFiles(root, globs, excludeDirs = new Set(), allowDotDirs = new Set()) {
18
+ const matched = readMatchingFiles(root, globs, excludeDirs, allowDotDirs);
19
+ const files = new Map();
20
+ for (const [path, content] of matched.files) {
21
+ if (globListSelects(path, globs))
22
+ files.set(path, content);
23
+ }
24
+ return { files, unread: Object.freeze(unreadMatching(matched.unread, globs)) };
25
+ }
@@ -0,0 +1,12 @@
1
+ import { type ISettledUnitLiveness } from '@shrkcrft/core';
2
+ import type { IGlobLivenessRequest } from './i-glob-liveness-request.js';
3
+ /**
4
+ * Settle a gate rule's glob lists through core's one authority,
5
+ * `settleUnitLiveness`, over THE gate-plane observation predicate
6
+ * (`globListLivenessInput`) — the call every plane engine makes before it
7
+ * decides what an empty rule is (`settleRuleEmptiness` takes the result as its
8
+ * `liveness`). `settled.dead` holds UNMARKED dead units only; a marked glob is
9
+ * intended-empty or went-live, and `settled.acceptance` is its printed record.
10
+ */
11
+ export declare function settleGlobLists(request: IGlobLivenessRequest): ISettledUnitLiveness;
12
+ //# sourceMappingURL=settle-glob-lists.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle-glob-lists.d.ts","sourceRoot":"","sources":["../../src/util/settle-glob-lists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE/E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAEnF"}
@@ -0,0 +1,13 @@
1
+ import { settleUnitLiveness } from '@shrkcrft/core';
2
+ import { globListLivenessInput } from "./glob-list-liveness-input.js";
3
+ /**
4
+ * Settle a gate rule's glob lists through core's one authority,
5
+ * `settleUnitLiveness`, over THE gate-plane observation predicate
6
+ * (`globListLivenessInput`) — the call every plane engine makes before it
7
+ * decides what an empty rule is (`settleRuleEmptiness` takes the result as its
8
+ * `liveness`). `settled.dead` holds UNMARKED dead units only; a marked glob is
9
+ * intended-empty or went-live, and `settled.acceptance` is its printed record.
10
+ */
11
+ export function settleGlobLists(request) {
12
+ return settleUnitLiveness(globListLivenessInput(request));
13
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Why the one reader (`readMatchingFiles`) matched a file against a rule's
3
+ * globs but did not read it. Either way the file is IN the rule's scope and
4
+ * was never examined, which is a coverage gap. It is never narrowing.
5
+ */
6
+ export declare enum UnreadFileReason {
7
+ /** Larger than `MAX_SCAN_FILE_BYTES`, so the reader skipped it. */
8
+ OverReadCap = "over-read-cap",
9
+ /**
10
+ * A regenerated file (`generated check`'s temp tree) larger than
11
+ * `MAX_REGEN_FILE_BYTES`, so it was never read or byte-compared.
12
+ */
13
+ OverRegenCap = "over-regen-cap",
14
+ /** The stat or the read failed (permissions, or a delete racing the walk). */
15
+ Unreadable = "unreadable",
16
+ /**
17
+ * A DIRECTORY the walk could not list (permissions): no file beneath it was
18
+ * ever matched, let alone read. The entry's `path` ends in `/` (the root is
19
+ * `./`), and it is in front of every rule whose globs could match beneath it
20
+ * — `unreadEntryMatches`, the one test every plane uses. It used to be a
21
+ * silent `return`, so a violation under a `chmod 000` directory read as a
22
+ * clean `0` with full coverage.
23
+ */
24
+ UnreadableDirectory = "unreadable-directory"
25
+ }
26
+ //# sourceMappingURL=unread-file-reason.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unread-file-reason.d.ts","sourceRoot":"","sources":["../../src/util/unread-file-reason.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,oBAAY,gBAAgB;IAC1B,mEAAmE;IACnE,WAAW,kBAAkB;IAC7B;;;OAGG;IACH,YAAY,mBAAmB;IAC/B,8EAA8E;IAC9E,UAAU,eAAe;IACzB;;;;;;;OAOG;IACH,mBAAmB,yBAAyB;CAC7C"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Why the one reader (`readMatchingFiles`) matched a file against a rule's
3
+ * globs but did not read it. Either way the file is IN the rule's scope and
4
+ * was never examined, which is a coverage gap. It is never narrowing.
5
+ */
6
+ export var UnreadFileReason;
7
+ (function (UnreadFileReason) {
8
+ /** Larger than `MAX_SCAN_FILE_BYTES`, so the reader skipped it. */
9
+ UnreadFileReason["OverReadCap"] = "over-read-cap";
10
+ /**
11
+ * A regenerated file (`generated check`'s temp tree) larger than
12
+ * `MAX_REGEN_FILE_BYTES`, so it was never read or byte-compared.
13
+ */
14
+ UnreadFileReason["OverRegenCap"] = "over-regen-cap";
15
+ /** The stat or the read failed (permissions, or a delete racing the walk). */
16
+ UnreadFileReason["Unreadable"] = "unreadable";
17
+ /**
18
+ * A DIRECTORY the walk could not list (permissions): no file beneath it was
19
+ * ever matched, let alone read. The entry's `path` ends in `/` (the root is
20
+ * `./`), and it is in front of every rule whose globs could match beneath it
21
+ * — `unreadEntryMatches`, the one test every plane uses. It used to be a
22
+ * silent `return`, so a violation under a `chmod 000` directory read as a
23
+ * clean `0` with full coverage.
24
+ */
25
+ UnreadFileReason["UnreadableDirectory"] = "unreadable-directory";
26
+ })(UnreadFileReason || (UnreadFileReason = {}));
@@ -0,0 +1,10 @@
1
+ import type { UnreadFileReason } from './unread-file-reason.js';
2
+ /** A file a rule's globs matched that the one reader did not read. */
3
+ export interface IUnreadFile {
4
+ /** Project-relative POSIX path. */
5
+ readonly path: string;
6
+ readonly reason: UnreadFileReason;
7
+ /** Size on disk, when the stat succeeded. */
8
+ readonly bytes?: number;
9
+ }
10
+ //# sourceMappingURL=unread-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unread-file.d.ts","sourceRoot":"","sources":["../../src/util/unread-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,6CAA6C;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,13 +1,96 @@
1
+ import type { IMatchedFiles } from './matched-files.js';
1
2
  /** Vendor / build / VCS dirs never scanned. */
2
3
  export declare const SKIP_DIRS: ReadonlySet<string>;
3
- /** Files larger than this are skipped (regex token extraction over multi-MB blobs is pointless). */
4
+ /**
5
+ * Files larger than this are not READ (regex token extraction over multi-MB
6
+ * blobs is pointless). They are still MATCHED: `readMatchingFiles` reports
7
+ * each one as unread, and every coverage record counts it as unexamined.
8
+ */
4
9
  export declare const MAX_SCAN_FILE_BYTES = 1000000;
5
10
  /**
6
- * Walk `root`, returning project-relative POSIX paths that match any glob.
7
- * `excludeDirs` is a set of project-relative POSIX directory paths to prune
8
- * entirely (e.g. the SharkCraft asset/config dir).
11
+ * The cap on one file of a REGENERATED temp tree (`generated check` /
12
+ * `generated update`): a runaway regen must not be read into memory whole. A
13
+ * file over it is reported unread (`UnreadFileReason.OverRegenCap`) by
14
+ * `readRegenTree`, never dropped.
15
+ */
16
+ export declare const MAX_REGEN_FILE_BYTES = 2000000;
17
+ /**
18
+ * THE rule for which directories the walk never enters: a vendor/build/VCS
19
+ * dir (`SKIP_DIRS`, absolute), an `excludeDirs` entry (project-relative), or a
20
+ * dot-directory the caller's globs did not name (`allowDotDirs`).
21
+ *
22
+ * A file under a skipped directory is outside every plane's scope by design,
23
+ * and is neither read nor reported unread. Anything that asks "would the walk
24
+ * reach this path?" must ask this predicate; it must not keep its own copy of
25
+ * the list. (Round 11: run-policy used to re-derive it to guess which unread
26
+ * changed paths were in scope. It now reads the reader's own unread list.)
27
+ */
28
+ export declare function walkSkipsDirectory(name: string, relPath: string, excludeDirs: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): boolean;
29
+ /**
30
+ * Walk `root`, returning project-relative POSIX paths that match any INCLUSION
31
+ * glob. `excludeDirs` is a set of project-relative POSIX directory paths to
32
+ * prune entirely (e.g. the SharkCraft asset/config dir).
33
+ *
34
+ * A walk is a POSITIVE union primitive: a `!` entry is dropped here, never
35
+ * applied. Planes walk the union of many rules' lists once, and a negation
36
+ * applied to that union would delete rule A's `!x` from rule B's scope — a
37
+ * silent under-selection. Each list's negations subtract per list, after the
38
+ * walk, through `globListSelects` (or `readSelectedFiles` for one list).
39
+ *
40
+ * `unlistedDirs`, when given, collects every directory the walk entered but
41
+ * could not LIST (a permission error — not a directory that vanished), as a
42
+ * project-relative path ending in `/` (the root is `./`). Every file beneath
43
+ * one was never matched; `readMatchingFiles` reports it unread rather than
44
+ * letting its files drop out of every rule's scope in silence.
45
+ */
46
+ export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>, unlistedDirs?: string[]): string[];
47
+ /** How many times `readMatchingFiles` walked + read the tree, and how many calls the memo served. */
48
+ export declare function readMatchingFilesStats(): {
49
+ readonly walks: number;
50
+ readonly memoHits: number;
51
+ };
52
+ /** Reset {@link readMatchingFilesStats}. */
53
+ export declare function resetReadMatchingFilesStats(): void;
54
+ /**
55
+ * Run `fn` with the read memo enabled, then clear it.
56
+ *
57
+ * ONLY wrap a scan that performs no writes of its own for its duration. A
58
+ * command the scan SPAWNS (a baseline's `compute.run`, a generated artifact's
59
+ * `regen`) may rewrite files, so every spawn site calls
60
+ * {@link clearFileReadCache} right after the child exits: a read after the
61
+ * spawn is then fresh, and reuse inside the window stays exact rather than
62
+ * merely probable.
63
+ */
64
+ export declare function withFileReadCache<T>(fn: () => T): T;
65
+ /** Drop every memoized read. For tests, and for a caller that has just written. */
66
+ export declare function clearFileReadCache(): void;
67
+ /**
68
+ * The dot-directory segments a glob set explicitly names.
69
+ *
70
+ * `.claude/skills/**` asks for `.claude`; `docs/**` asks for nothing. Deriving
71
+ * the allowlist from the globs themselves means a rule gets exactly the
72
+ * directories it named and no others — a blanket "scan dot-dirs" switch would
73
+ * wander into `.venv` and `.yarn` the moment someone wrote a recursive glob.
74
+ */
75
+ export declare function dotDirsNamedBy(globs: readonly string[]): Set<string>;
76
+ /**
77
+ * THE reader: walk + read every file matching `globs`.
78
+ *
79
+ * Returns the files it read AND the matched files it did not read (over
80
+ * {@link MAX_SCAN_FILE_BYTES}, or unreadable), each with its reason. It used
81
+ * to drop those silently. Every plane then counted its expected scope from
82
+ * what was read, so an over-cap file holding a forbidden token read
83
+ * "examined 1 of 1 ✓". Now the gap is part of the return value, and every
84
+ * engine folds it into its rule coverage through `readScopeCoverage`.
85
+ *
86
+ * A path deleted between the walk and the stat is no longer in scope and is
87
+ * not reported.
88
+ *
89
+ * POSITIVE-ONLY, like {@link walkMatching}: a `!` entry in `globs` never
90
+ * subtracts here, because callers hand this the union of many lists. The
91
+ * result is every file an inclusion glob matched; a caller reading ONE list
92
+ * selects through `readSelectedFiles`, a caller reading many filters each
93
+ * list with `globListSelects` / `unreadMatching`.
9
94
  */
10
- export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): string[];
11
- /** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
12
- export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): Map<string, string>;
95
+ export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): IMatchedFiles;
13
96
  //# sourceMappingURL=walk-files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"walk-files.d.ts","sourceRoot":"","sources":["../../src/util/walk-files.ts"],"names":[],"mappings":"AAIA,+CAA+C;AAC/C,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAUxC,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,UAAY,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,GAC3C,MAAM,EAAE,CA2BV;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,GAC3C,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrB"}
1
+ {"version":3,"file":"walk-files.d.ts","sourceRoot":"","sources":["../../src/util/walk-files.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,+CAA+C;AAC/C,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAUxC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,UAAY,CAAC;AAE7C;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAY,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,EAChC,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,OAAO,CAGT;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,EAC7C,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,MAAM,EAAE,CAqCV;AA0BD,qGAAqG;AACrG,wBAAgB,sBAAsB,IAAI;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAE9F;AAED,4CAA4C;AAC5C,wBAAgB,2BAA2B,IAAI,IAAI,CAGlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CASnD;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAGD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAWpE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,aAAa,CA8Df"}
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
- import { matchesAny } from "../scan/glob.js";
3
+ import { globListWalkGlobs, globMayMatchUnder, matchesAny } from "../scan/glob.js";
4
+ import { UnreadFileReason } from "./unread-file-reason.js";
4
5
  /** Vendor / build / VCS dirs never scanned. */
5
6
  export const SKIP_DIRS = new Set([
6
7
  'node_modules',
@@ -13,21 +14,66 @@ export const SKIP_DIRS = new Set([
13
14
  '.turbo',
14
15
  '.cache',
15
16
  ]);
16
- /** Files larger than this are skipped (regex token extraction over multi-MB blobs is pointless). */
17
+ /**
18
+ * Files larger than this are not READ (regex token extraction over multi-MB
19
+ * blobs is pointless). They are still MATCHED: `readMatchingFiles` reports
20
+ * each one as unread, and every coverage record counts it as unexamined.
21
+ */
17
22
  export const MAX_SCAN_FILE_BYTES = 1_000_000;
18
23
  /**
19
- * Walk `root`, returning project-relative POSIX paths that match any glob.
20
- * `excludeDirs` is a set of project-relative POSIX directory paths to prune
21
- * entirely (e.g. the SharkCraft asset/config dir).
24
+ * The cap on one file of a REGENERATED temp tree (`generated check` /
25
+ * `generated update`): a runaway regen must not be read into memory whole. A
26
+ * file over it is reported unread (`UnreadFileReason.OverRegenCap`) by
27
+ * `readRegenTree`, never dropped.
22
28
  */
23
- export function walkMatching(root, globs, excludeDirs = new Set()) {
29
+ export const MAX_REGEN_FILE_BYTES = 2_000_000;
30
+ /**
31
+ * THE rule for which directories the walk never enters: a vendor/build/VCS
32
+ * dir (`SKIP_DIRS`, absolute), an `excludeDirs` entry (project-relative), or a
33
+ * dot-directory the caller's globs did not name (`allowDotDirs`).
34
+ *
35
+ * A file under a skipped directory is outside every plane's scope by design,
36
+ * and is neither read nor reported unread. Anything that asks "would the walk
37
+ * reach this path?" must ask this predicate; it must not keep its own copy of
38
+ * the list. (Round 11: run-policy used to re-derive it to guess which unread
39
+ * changed paths were in scope. It now reads the reader's own unread list.)
40
+ */
41
+ export function walkSkipsDirectory(name, relPath, excludeDirs, allowDotDirs = new Set()) {
42
+ if (SKIP_DIRS.has(name) || excludeDirs.has(relPath))
43
+ return true;
44
+ return name.startsWith('.') && !allowDotDirs.has(name);
45
+ }
46
+ /**
47
+ * Walk `root`, returning project-relative POSIX paths that match any INCLUSION
48
+ * glob. `excludeDirs` is a set of project-relative POSIX directory paths to
49
+ * prune entirely (e.g. the SharkCraft asset/config dir).
50
+ *
51
+ * A walk is a POSITIVE union primitive: a `!` entry is dropped here, never
52
+ * applied. Planes walk the union of many rules' lists once, and a negation
53
+ * applied to that union would delete rule A's `!x` from rule B's scope — a
54
+ * silent under-selection. Each list's negations subtract per list, after the
55
+ * walk, through `globListSelects` (or `readSelectedFiles` for one list).
56
+ *
57
+ * `unlistedDirs`, when given, collects every directory the walk entered but
58
+ * could not LIST (a permission error — not a directory that vanished), as a
59
+ * project-relative path ending in `/` (the root is `./`). Every file beneath
60
+ * one was never matched; `readMatchingFiles` reports it unread rather than
61
+ * letting its files drop out of every rule's scope in silence.
62
+ */
63
+ export function walkMatching(root, globs, excludeDirs = new Set(), allowDotDirs = new Set(), unlistedDirs) {
64
+ const walkGlobs = globListWalkGlobs(globs);
24
65
  const out = [];
25
66
  const visit = (abs) => {
26
67
  let entries;
27
68
  try {
28
69
  entries = readdirSync(abs, { withFileTypes: true });
29
70
  }
30
- catch {
71
+ catch (e) {
72
+ const code = e.code;
73
+ if (unlistedDirs && code !== 'ENOENT' && code !== 'ENOTDIR') {
74
+ const rel = nodePath.relative(root, abs).split(nodePath.sep).join('/');
75
+ unlistedDirs.push(rel === '' ? './' : `${rel}/`);
76
+ }
31
77
  return;
32
78
  }
33
79
  for (const e of entries) {
@@ -39,12 +85,16 @@ export function walkMatching(root, globs, excludeDirs = new Set()) {
39
85
  // so neither policy-lint nor wiring scans tooling/vendored sources.
40
86
  // (Dirent.isDirectory() is false for symlinks, so symlinked dirs are
41
87
  // never descended — no loop risk.)
42
- if (e.name.startsWith('.') || SKIP_DIRS.has(e.name) || excludeDirs.has(rel))
88
+ // Dot-directories are vendored tooling by default (`.venv`, `.yarn`,
89
+ // `.gradle`). But a rule may legitimately target one — an agent skill
90
+ // file lives in `.claude/skills` — so a caller can name the dot-dirs
91
+ // its OWN globs ask for. `SKIP_DIRS` stays absolute either way.
92
+ if (walkSkipsDirectory(e.name, rel, excludeDirs, allowDotDirs))
43
93
  continue;
44
94
  visit(childAbs);
45
95
  }
46
96
  else if (e.isFile()) {
47
- if (matchesAny(rel, globs))
97
+ if (matchesAny(rel, walkGlobs))
48
98
  out.push(rel);
49
99
  }
50
100
  }
@@ -52,29 +102,166 @@ export function walkMatching(root, globs, excludeDirs = new Set()) {
52
102
  visit(root);
53
103
  return out;
54
104
  }
55
- /** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
56
- export function readMatchingFiles(root, globs, excludeDirs = new Set()) {
57
- const out = new Map();
58
- for (const rel of walkMatching(root, globs, excludeDirs)) {
105
+ /**
106
+ * Memo of `readMatchingFiles`, keyed by (root, glob set, excludes).
107
+ *
108
+ * One coverage run resolves the SAME globs many times over: every rule on every
109
+ * plane walks the tree for its own sources, and a shared `$use` extractor is by
110
+ * construction read once per consumer. The walk + read is roughly half the cost
111
+ * of a wide extraction, so memoizing turns N walks into one.
112
+ *
113
+ * It is OFF by default and enabled only by {@link withFileReadCache}, around a
114
+ * scan that provably neither spawns nor writes. That restriction is not
115
+ * caution for its own sake — a global memo really does hand back a stale
116
+ * snapshot when the same process writes a file between two scans, and a trust
117
+ * tool answering "nothing drifted" from a stale read is worse than a slow one.
118
+ * Making the safe window explicit means the condition cannot be forgotten at a
119
+ * call site.
120
+ */
121
+ const READ_MEMO = new Map();
122
+ /** True only inside {@link withFileReadCache}. */
123
+ let memoEnabled = false;
124
+ /** Tree walks `readMatchingFiles` performed, and walks the memo served (tests / perf locks). */
125
+ let walkCount = 0;
126
+ let readMemoHits = 0;
127
+ /** How many times `readMatchingFiles` walked + read the tree, and how many calls the memo served. */
128
+ export function readMatchingFilesStats() {
129
+ return { walks: walkCount, memoHits: readMemoHits };
130
+ }
131
+ /** Reset {@link readMatchingFilesStats}. */
132
+ export function resetReadMatchingFilesStats() {
133
+ walkCount = 0;
134
+ readMemoHits = 0;
135
+ }
136
+ /**
137
+ * Run `fn` with the read memo enabled, then clear it.
138
+ *
139
+ * ONLY wrap a scan that performs no writes of its own for its duration. A
140
+ * command the scan SPAWNS (a baseline's `compute.run`, a generated artifact's
141
+ * `regen`) may rewrite files, so every spawn site calls
142
+ * {@link clearFileReadCache} right after the child exits: a read after the
143
+ * spawn is then fresh, and reuse inside the window stays exact rather than
144
+ * merely probable.
145
+ */
146
+ export function withFileReadCache(fn) {
147
+ const previous = memoEnabled;
148
+ memoEnabled = true;
149
+ try {
150
+ return fn();
151
+ }
152
+ finally {
153
+ memoEnabled = previous;
154
+ if (!previous)
155
+ READ_MEMO.clear();
156
+ }
157
+ }
158
+ /** Drop every memoized read. For tests, and for a caller that has just written. */
159
+ export function clearFileReadCache() {
160
+ READ_MEMO.clear();
161
+ }
162
+ /**
163
+ * The dot-directory segments a glob set explicitly names.
164
+ *
165
+ * `.claude/skills/**` asks for `.claude`; `docs/**` asks for nothing. Deriving
166
+ * the allowlist from the globs themselves means a rule gets exactly the
167
+ * directories it named and no others — a blanket "scan dot-dirs" switch would
168
+ * wander into `.venv` and `.yarn` the moment someone wrote a recursive glob.
169
+ */
170
+ export function dotDirsNamedBy(globs) {
171
+ const out = new Set();
172
+ // Only an inclusion glob can put a file in scope, so only it opens a dot-dir.
173
+ for (const glob of globListWalkGlobs(globs)) {
174
+ for (const segment of glob.split('/')) {
175
+ if (segment.startsWith('.') && segment.length > 1 && !segment.includes('*') && !segment.includes('?')) {
176
+ out.add(segment);
177
+ }
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+ /**
183
+ * THE reader: walk + read every file matching `globs`.
184
+ *
185
+ * Returns the files it read AND the matched files it did not read (over
186
+ * {@link MAX_SCAN_FILE_BYTES}, or unreadable), each with its reason. It used
187
+ * to drop those silently. Every plane then counted its expected scope from
188
+ * what was read, so an over-cap file holding a forbidden token read
189
+ * "examined 1 of 1 ✓". Now the gap is part of the return value, and every
190
+ * engine folds it into its rule coverage through `readScopeCoverage`.
191
+ *
192
+ * A path deleted between the walk and the stat is no longer in scope and is
193
+ * not reported.
194
+ *
195
+ * POSITIVE-ONLY, like {@link walkMatching}: a `!` entry in `globs` never
196
+ * subtracts here, because callers hand this the union of many lists. The
197
+ * result is every file an inclusion glob matched; a caller reading ONE list
198
+ * selects through `readSelectedFiles`, a caller reading many filters each
199
+ * list with `globListSelects` / `unreadMatching`.
200
+ */
201
+ export function readMatchingFiles(root, globs, excludeDirs = new Set(), allowDotDirs = new Set()) {
202
+ // `['src/**', '!src/x.ts']` and `['src/**']` are one walk, so one memo entry.
203
+ const walkGlobs = globListWalkGlobs(globs);
204
+ const key = memoEnabled
205
+ ? `${root}\u0000${[...walkGlobs].sort().join('\u0001')}\u0000${[...excludeDirs].sort().join('\u0001')}` +
206
+ `\u0000${[...allowDotDirs].sort().join('\u0001')}`
207
+ : undefined;
208
+ if (key !== undefined) {
209
+ const hit = READ_MEMO.get(key);
210
+ // Hand back a COPY of the map: callers routinely mutate the one they get
211
+ // (the wiring scan filters it, the generated scan partitions it), and a
212
+ // shared instance would let one rule's bookkeeping corrupt the next
213
+ // rule's inputs. The unread list is frozen, so it is shared as is.
214
+ if (hit) {
215
+ readMemoHits += 1;
216
+ return { files: new Map(hit.files), unread: hit.unread };
217
+ }
218
+ }
219
+ walkCount += 1;
220
+ const files = new Map();
221
+ const unread = [];
222
+ const unlistedDirs = [];
223
+ const matchedPaths = walkMatching(root, walkGlobs, excludeDirs, allowDotDirs, unlistedDirs);
224
+ // A directory the walk could not list hides every file beneath it. It is in
225
+ // front of any glob that could match there, so it is reported unread (the
226
+ // rule over that tree settles PARTIAL), never dropped from scope in silence.
227
+ // Only an inclusion glob can be in front of it: `!**/x` has a `**` segment,
228
+ // but a negation selects nothing on its own.
229
+ for (const dir of unlistedDirs) {
230
+ if (walkGlobs.some((g) => globMayMatchUnder(g, dir))) {
231
+ unread.push({ path: dir, reason: UnreadFileReason.UnreadableDirectory });
232
+ }
233
+ }
234
+ for (const rel of matchedPaths) {
59
235
  const abs = nodePath.join(root, rel);
60
- let size = -1;
236
+ let size;
61
237
  try {
62
238
  const st = statSync(abs);
63
239
  if (!st.isFile())
64
240
  continue;
65
241
  size = st.size;
66
242
  }
67
- catch {
243
+ catch (e) {
244
+ // Deleted since the walk saw it: gone, so out of scope. Anything else
245
+ // (a permission error) leaves a matched file unexamined.
246
+ if (e.code !== 'ENOENT') {
247
+ unread.push({ path: rel, reason: UnreadFileReason.Unreadable });
248
+ }
68
249
  continue;
69
250
  }
70
- if (size > MAX_SCAN_FILE_BYTES)
251
+ if (size > MAX_SCAN_FILE_BYTES) {
252
+ unread.push({ path: rel, reason: UnreadFileReason.OverReadCap, bytes: size });
71
253
  continue;
254
+ }
72
255
  try {
73
- out.set(rel, readFileSync(abs, 'utf8'));
256
+ files.set(rel, readFileSync(abs, 'utf8'));
74
257
  }
75
258
  catch {
76
- // unreadable — skip
259
+ unread.push({ path: rel, reason: UnreadFileReason.Unreadable, bytes: size });
77
260
  }
78
261
  }
79
- return out;
262
+ unread.sort((a, b) => a.path.localeCompare(b.path));
263
+ const result = { files, unread: Object.freeze(unread) };
264
+ if (key !== undefined)
265
+ READ_MEMO.set(key, { files: new Map(files), unread: result.unread });
266
+ return result;
80
267
  }