@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,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,19 +1,65 @@
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.
9
45
  */
10
- export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): string[];
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;
11
54
  /**
12
55
  * Run `fn` with the read memo enabled, then clear it.
13
56
  *
14
- * ONLY wrap a scan that performs no writes and spawns no commands for its
15
- * duration — within such a window the tree cannot change beneath the memo, so
16
- * reuse is exact rather than merely probable.
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.
17
63
  */
18
64
  export declare function withFileReadCache<T>(fn: () => T): T;
19
65
  /** Drop every memoized read. For tests, and for a caller that has just written. */
@@ -27,6 +73,24 @@ export declare function clearFileReadCache(): void;
27
73
  * wander into `.venv` and `.yarn` the moment someone wrote a recursive glob.
28
74
  */
29
75
  export declare function dotDirsNamedBy(globs: readonly string[]): Set<string>;
30
- /** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
31
- export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): Map<string, 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`.
94
+ */
95
+ export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): IMatchedFiles;
32
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,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,MAAM,EAAE,CAgCV;AAuBD;;;;;;GAMG;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,CAUpE;AAED,oFAAoF;AACpF,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,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgCrB"}
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.
28
+ */
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.
22
62
  */
23
- export function walkMatching(root, globs, excludeDirs = new Set(), allowDotDirs = new Set()) {
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) {
@@ -43,14 +89,12 @@ export function walkMatching(root, globs, excludeDirs = new Set(), allowDotDirs
43
89
  // `.gradle`). But a rule may legitimately target one — an agent skill
44
90
  // file lives in `.claude/skills` — so a caller can name the dot-dirs
45
91
  // its OWN globs ask for. `SKIP_DIRS` stays absolute either way.
46
- if (SKIP_DIRS.has(e.name) || excludeDirs.has(rel))
47
- continue;
48
- if (e.name.startsWith('.') && !allowDotDirs.has(e.name))
92
+ if (walkSkipsDirectory(e.name, rel, excludeDirs, allowDotDirs))
49
93
  continue;
50
94
  visit(childAbs);
51
95
  }
52
96
  else if (e.isFile()) {
53
- if (matchesAny(rel, globs))
97
+ if (matchesAny(rel, walkGlobs))
54
98
  out.push(rel);
55
99
  }
56
100
  }
@@ -77,12 +121,27 @@ export function walkMatching(root, globs, excludeDirs = new Set(), allowDotDirs
77
121
  const READ_MEMO = new Map();
78
122
  /** True only inside {@link withFileReadCache}. */
79
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
+ }
80
136
  /**
81
137
  * Run `fn` with the read memo enabled, then clear it.
82
138
  *
83
- * ONLY wrap a scan that performs no writes and spawns no commands for its
84
- * duration — within such a window the tree cannot change beneath the memo, so
85
- * reuse is exact rather than merely probable.
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.
86
145
  */
87
146
  export function withFileReadCache(fn) {
88
147
  const previous = memoEnabled;
@@ -110,7 +169,8 @@ export function clearFileReadCache() {
110
169
  */
111
170
  export function dotDirsNamedBy(globs) {
112
171
  const out = new Set();
113
- for (const glob of globs) {
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)) {
114
174
  for (const segment of glob.split('/')) {
115
175
  if (segment.startsWith('.') && segment.length > 1 && !segment.includes('*') && !segment.includes('?')) {
116
176
  out.add(segment);
@@ -119,43 +179,89 @@ export function dotDirsNamedBy(globs) {
119
179
  }
120
180
  return out;
121
181
  }
122
- /** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
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
+ */
123
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);
124
204
  const key = memoEnabled
125
- ? `${root}\u0000${[...globs].sort().join('\u0001')}\u0000${[...excludeDirs].sort().join('\u0001')}` +
205
+ ? `${root}\u0000${[...walkGlobs].sort().join('\u0001')}\u0000${[...excludeDirs].sort().join('\u0001')}` +
126
206
  `\u0000${[...allowDotDirs].sort().join('\u0001')}`
127
207
  : undefined;
128
208
  if (key !== undefined) {
129
209
  const hit = READ_MEMO.get(key);
130
- // Hand back a COPY: callers routinely mutate the map they get (the wiring
131
- // scan filters it, the generated scan partitions it), and a shared instance
132
- // would let one rule's bookkeeping corrupt the next rule's inputs.
133
- if (hit)
134
- return new Map(hit);
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
+ }
135
218
  }
136
- const out = new Map();
137
- for (const rel of walkMatching(root, globs, excludeDirs, allowDotDirs)) {
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) {
138
235
  const abs = nodePath.join(root, rel);
139
- let size = -1;
236
+ let size;
140
237
  try {
141
238
  const st = statSync(abs);
142
239
  if (!st.isFile())
143
240
  continue;
144
241
  size = st.size;
145
242
  }
146
- 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
+ }
147
249
  continue;
148
250
  }
149
- if (size > MAX_SCAN_FILE_BYTES)
251
+ if (size > MAX_SCAN_FILE_BYTES) {
252
+ unread.push({ path: rel, reason: UnreadFileReason.OverReadCap, bytes: size });
150
253
  continue;
254
+ }
151
255
  try {
152
- out.set(rel, readFileSync(abs, 'utf8'));
256
+ files.set(rel, readFileSync(abs, 'utf8'));
153
257
  }
154
258
  catch {
155
- // unreadable — skip
259
+ unread.push({ path: rel, reason: UnreadFileReason.Unreadable, bytes: size });
156
260
  }
157
261
  }
262
+ unread.sort((a, b) => a.path.localeCompare(b.path));
263
+ const result = { files, unread: Object.freeze(unread) };
158
264
  if (key !== undefined)
159
- READ_MEMO.set(key, new Map(out));
160
- return out;
265
+ READ_MEMO.set(key, { files: new Map(files), unread: result.unread });
266
+ return result;
161
267
  }