@smthrs/plan 0.0.0-stage → 1.0.0-rc.3

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 (199) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/dist/cjs/CachePolicy.d.ts +131 -0
  5. package/dist/cjs/CachePolicy.d.ts.map +1 -0
  6. package/dist/cjs/CachePolicy.js +50 -0
  7. package/dist/cjs/CachePolicy.js.map +7 -0
  8. package/dist/cjs/Effects.d.ts +240 -0
  9. package/dist/cjs/Effects.d.ts.map +1 -0
  10. package/dist/cjs/Effects.js +76 -0
  11. package/dist/cjs/Effects.js.map +7 -0
  12. package/dist/cjs/FileSet.d.ts +288 -0
  13. package/dist/cjs/FileSet.d.ts.map +1 -0
  14. package/dist/cjs/FileSet.js +191 -0
  15. package/dist/cjs/FileSet.js.map +7 -0
  16. package/dist/cjs/GraphBuildError.d.ts +122 -0
  17. package/dist/cjs/GraphBuildError.d.ts.map +1 -0
  18. package/dist/cjs/GraphBuildError.js +71 -0
  19. package/dist/cjs/GraphBuildError.js.map +7 -0
  20. package/dist/cjs/KeyMaterial.d.ts +102 -0
  21. package/dist/cjs/KeyMaterial.d.ts.map +1 -0
  22. package/dist/cjs/KeyMaterial.js +64 -0
  23. package/dist/cjs/KeyMaterial.js.map +7 -0
  24. package/dist/cjs/Node.d.ts +606 -0
  25. package/dist/cjs/Node.d.ts.map +1 -0
  26. package/dist/cjs/Node.js +207 -0
  27. package/dist/cjs/Node.js.map +7 -0
  28. package/dist/cjs/Placement.d.ts +117 -0
  29. package/dist/cjs/Placement.d.ts.map +1 -0
  30. package/dist/cjs/Placement.js +46 -0
  31. package/dist/cjs/Placement.js.map +7 -0
  32. package/dist/cjs/Plan.d.ts +440 -0
  33. package/dist/cjs/Plan.d.ts.map +1 -0
  34. package/dist/cjs/Plan.js +508 -0
  35. package/dist/cjs/Plan.js.map +7 -0
  36. package/dist/cjs/PlanDiff.d.ts +58 -0
  37. package/dist/cjs/PlanDiff.d.ts.map +1 -0
  38. package/dist/cjs/PlanDiff.js +124 -0
  39. package/dist/cjs/PlanDiff.js.map +7 -0
  40. package/dist/cjs/Planned.d.ts +121 -0
  41. package/dist/cjs/Planned.d.ts.map +1 -0
  42. package/dist/cjs/Planned.js +72 -0
  43. package/dist/cjs/Planned.js.map +7 -0
  44. package/dist/cjs/Repetition.d.ts +31 -0
  45. package/dist/cjs/Repetition.d.ts.map +1 -0
  46. package/dist/cjs/Repetition.js +36 -0
  47. package/dist/cjs/Repetition.js.map +7 -0
  48. package/dist/cjs/Scheduling.d.ts +89 -0
  49. package/dist/cjs/Scheduling.d.ts.map +1 -0
  50. package/dist/cjs/Scheduling.js +76 -0
  51. package/dist/cjs/Scheduling.js.map +7 -0
  52. package/dist/cjs/StepKey.d.ts +333 -0
  53. package/dist/cjs/StepKey.d.ts.map +1 -0
  54. package/dist/cjs/StepKey.js +311 -0
  55. package/dist/cjs/StepKey.js.map +7 -0
  56. package/dist/cjs/index.d.ts +91 -0
  57. package/dist/cjs/index.d.ts.map +1 -0
  58. package/dist/cjs/index.js +59 -0
  59. package/dist/cjs/index.js.map +7 -0
  60. package/dist/cjs/internal/ConflictAnnotation.d.ts +59 -0
  61. package/dist/cjs/internal/ConflictAnnotation.d.ts.map +1 -0
  62. package/dist/cjs/internal/ConflictAnnotation.js +247 -0
  63. package/dist/cjs/internal/ConflictAnnotation.js.map +7 -0
  64. package/dist/cjs/internal/EffectCandidates.d.ts +19 -0
  65. package/dist/cjs/internal/EffectCandidates.d.ts.map +1 -0
  66. package/dist/cjs/internal/EffectCandidates.js +92 -0
  67. package/dist/cjs/internal/EffectCandidates.js.map +7 -0
  68. package/dist/cjs/internal/JsonMirror.d.ts +34 -0
  69. package/dist/cjs/internal/JsonMirror.d.ts.map +1 -0
  70. package/dist/cjs/internal/JsonMirror.js +195 -0
  71. package/dist/cjs/internal/JsonMirror.js.map +7 -0
  72. package/dist/cjs/internal/effects.d.ts +245 -0
  73. package/dist/cjs/internal/effects.d.ts.map +1 -0
  74. package/dist/cjs/internal/effects.js +290 -0
  75. package/dist/cjs/internal/effects.js.map +7 -0
  76. package/dist/cjs/internal/node.d.ts +526 -0
  77. package/dist/cjs/internal/node.d.ts.map +1 -0
  78. package/dist/cjs/internal/node.js +312 -0
  79. package/dist/cjs/internal/node.js.map +7 -0
  80. package/dist/cjs/package.json +1 -0
  81. package/dist/cjs/test/PlanFixtures.d.ts +127 -0
  82. package/dist/cjs/test/PlanFixtures.d.ts.map +1 -0
  83. package/dist/cjs/test/PlanFixtures.js +59 -0
  84. package/dist/cjs/test/PlanFixtures.js.map +7 -0
  85. package/dist/esm/CachePolicy.d.ts +131 -0
  86. package/dist/esm/CachePolicy.d.ts.map +1 -0
  87. package/dist/esm/CachePolicy.js +117 -0
  88. package/dist/esm/CachePolicy.js.map +1 -0
  89. package/dist/esm/Effects.d.ts +240 -0
  90. package/dist/esm/Effects.d.ts.map +1 -0
  91. package/dist/esm/Effects.js +167 -0
  92. package/dist/esm/Effects.js.map +1 -0
  93. package/dist/esm/FileSet.d.ts +288 -0
  94. package/dist/esm/FileSet.d.ts.map +1 -0
  95. package/dist/esm/FileSet.js +324 -0
  96. package/dist/esm/FileSet.js.map +1 -0
  97. package/dist/esm/GraphBuildError.d.ts +122 -0
  98. package/dist/esm/GraphBuildError.d.ts.map +1 -0
  99. package/dist/esm/GraphBuildError.js +146 -0
  100. package/dist/esm/GraphBuildError.js.map +1 -0
  101. package/dist/esm/KeyMaterial.d.ts +102 -0
  102. package/dist/esm/KeyMaterial.d.ts.map +1 -0
  103. package/dist/esm/KeyMaterial.js +85 -0
  104. package/dist/esm/KeyMaterial.js.map +1 -0
  105. package/dist/esm/Node.d.ts +606 -0
  106. package/dist/esm/Node.d.ts.map +1 -0
  107. package/dist/esm/Node.js +521 -0
  108. package/dist/esm/Node.js.map +1 -0
  109. package/dist/esm/Placement.d.ts +117 -0
  110. package/dist/esm/Placement.d.ts.map +1 -0
  111. package/dist/esm/Placement.js +65 -0
  112. package/dist/esm/Placement.js.map +1 -0
  113. package/dist/esm/Plan.d.ts +440 -0
  114. package/dist/esm/Plan.d.ts.map +1 -0
  115. package/dist/esm/Plan.js +673 -0
  116. package/dist/esm/Plan.js.map +1 -0
  117. package/dist/esm/PlanDiff.d.ts +58 -0
  118. package/dist/esm/PlanDiff.d.ts.map +1 -0
  119. package/dist/esm/PlanDiff.js +159 -0
  120. package/dist/esm/PlanDiff.js.map +1 -0
  121. package/dist/esm/Planned.d.ts +121 -0
  122. package/dist/esm/Planned.d.ts.map +1 -0
  123. package/dist/esm/Planned.js +147 -0
  124. package/dist/esm/Planned.js.map +1 -0
  125. package/dist/esm/Repetition.d.ts +31 -0
  126. package/dist/esm/Repetition.d.ts.map +1 -0
  127. package/dist/esm/Repetition.js +24 -0
  128. package/dist/esm/Repetition.js.map +1 -0
  129. package/dist/esm/Scheduling.d.ts +89 -0
  130. package/dist/esm/Scheduling.d.ts.map +1 -0
  131. package/dist/esm/Scheduling.js +82 -0
  132. package/dist/esm/Scheduling.js.map +1 -0
  133. package/dist/esm/StepKey.d.ts +333 -0
  134. package/dist/esm/StepKey.d.ts.map +1 -0
  135. package/dist/esm/StepKey.js +478 -0
  136. package/dist/esm/StepKey.js.map +1 -0
  137. package/dist/esm/index.d.ts +91 -0
  138. package/dist/esm/index.d.ts.map +1 -0
  139. package/dist/esm/index.js +91 -0
  140. package/dist/esm/index.js.map +1 -0
  141. package/dist/esm/internal/ConflictAnnotation.d.ts +59 -0
  142. package/dist/esm/internal/ConflictAnnotation.d.ts.map +1 -0
  143. package/dist/esm/internal/ConflictAnnotation.js +332 -0
  144. package/dist/esm/internal/ConflictAnnotation.js.map +1 -0
  145. package/dist/esm/internal/EffectCandidates.d.ts +19 -0
  146. package/dist/esm/internal/EffectCandidates.d.ts.map +1 -0
  147. package/dist/esm/internal/EffectCandidates.js +86 -0
  148. package/dist/esm/internal/EffectCandidates.js.map +1 -0
  149. package/dist/esm/internal/JsonMirror.d.ts +34 -0
  150. package/dist/esm/internal/JsonMirror.d.ts.map +1 -0
  151. package/dist/esm/internal/JsonMirror.js +204 -0
  152. package/dist/esm/internal/JsonMirror.js.map +1 -0
  153. package/dist/esm/internal/effects.d.ts +245 -0
  154. package/dist/esm/internal/effects.d.ts.map +1 -0
  155. package/dist/esm/internal/effects.js +459 -0
  156. package/dist/esm/internal/effects.js.map +1 -0
  157. package/dist/esm/internal/node.d.ts +526 -0
  158. package/dist/esm/internal/node.d.ts.map +1 -0
  159. package/dist/esm/internal/node.js +564 -0
  160. package/dist/esm/internal/node.js.map +1 -0
  161. package/dist/esm/test/PlanFixtures.d.ts +127 -0
  162. package/dist/esm/test/PlanFixtures.d.ts.map +1 -0
  163. package/dist/esm/test/PlanFixtures.js +60 -0
  164. package/dist/esm/test/PlanFixtures.js.map +1 -0
  165. package/docs/README.md +165 -0
  166. package/docs/api.md +1475 -0
  167. package/docs/concepts/authoring-ast.md +203 -0
  168. package/docs/concepts/effects-and-conflicts.md +135 -0
  169. package/docs/concepts/plan-value.md +168 -0
  170. package/docs/concepts/step-keys.md +179 -0
  171. package/docs/guides/author-a-node-graph.md +182 -0
  172. package/docs/guides/compile-a-plan.md +146 -0
  173. package/docs/guides/declare-file-effects.md +143 -0
  174. package/docs/guides/diff-two-plans.md +101 -0
  175. package/docs/installation.md +71 -0
  176. package/docs/quickstart.md +128 -0
  177. package/docs/testing.md +73 -0
  178. package/docs/troubleshooting.md +314 -0
  179. package/package.json +254 -3
  180. package/src/CachePolicy.ts +143 -0
  181. package/src/Effects.ts +284 -0
  182. package/src/FileSet.ts +411 -0
  183. package/src/GraphBuildError.ts +158 -0
  184. package/src/KeyMaterial.ts +106 -0
  185. package/src/Node.ts +871 -0
  186. package/src/Placement.ts +99 -0
  187. package/src/Plan.ts +868 -0
  188. package/src/PlanDiff.ts +199 -0
  189. package/src/Planned.ts +208 -0
  190. package/src/Repetition.ts +33 -0
  191. package/src/Scheduling.ts +154 -0
  192. package/src/StepKey.ts +710 -0
  193. package/src/index.ts +103 -0
  194. package/src/internal/ConflictAnnotation.ts +374 -0
  195. package/src/internal/EffectCandidates.ts +89 -0
  196. package/src/internal/JsonMirror.ts +255 -0
  197. package/src/internal/effects.ts +543 -0
  198. package/src/internal/node.ts +858 -0
  199. package/src/test/PlanFixtures.ts +76 -0
@@ -0,0 +1,543 @@
1
+ /**
2
+ * Path indexing shared by effect matching and the graph's write-conflict pass.
3
+ *
4
+ * The pattern grammar is exhaustive: an entry ending in `*` matches by string
5
+ * prefix and every other entry matches itself alone. Every path is read a
6
+ * bounded number of times: once to look for a dot segment, once to detect a
7
+ * pattern, and once per comparison a binary search needs while the distinct
8
+ * paths are sorted and each pattern's prefix is located. After that a path is
9
+ * an integer rank and every match is an integer comparison, so matching two
10
+ * declarations costs their combined length plus the matches, whatever the
11
+ * paths' lengths or how many patterns nest. An envelope is prepared once for
12
+ * every step it encloses: its exact entries go into a set and its covering
13
+ * patterns collapse to disjoint sorted prefixes, so a step path costs one
14
+ * lookup and one binary search however wide the envelope is.
15
+ *
16
+ * Governing contract: `packages/smithers/flows/plan/docs/api.md`, published as
17
+ * https://smithers.sh/docs/reference/api/plan.
18
+ *
19
+ * @since 1.0.0-rc.0
20
+ */
21
+
22
+ import type * as Effects from "../Effects.ts"
23
+
24
+ const SLASH = 47
25
+ const DOT = 46
26
+ const STAR = 42
27
+
28
+ /**
29
+ * Whether `path` contains a whole `.` or `..` segment.
30
+ *
31
+ * A path with no `.` costs one native search. Otherwise the segments from the
32
+ * one holding the first `.` onward are scanned once, without allocating.
33
+ *
34
+ * @since 1.0.0-rc.0
35
+ * @private
36
+ */
37
+ export const hasDotSegment = (path: string): boolean => {
38
+ const first = path.indexOf(".")
39
+ if (first === -1) return false
40
+ const length = path.length
41
+ let start = path.lastIndexOf("/", first) + 1
42
+ for (let index = start; index <= length; index++) {
43
+ if (index === length || path.charCodeAt(index) === SLASH) {
44
+ const size = index - start
45
+ if (size === 1 && path.charCodeAt(start) === DOT) return true
46
+ if (size === 2 && path.charCodeAt(start) === DOT && path.charCodeAt(start + 1) === DOT) return true
47
+ start = index + 1
48
+ }
49
+ }
50
+ return false
51
+ }
52
+
53
+ /**
54
+ * Whether an entry is a pattern: only a trailing `*` makes one.
55
+ *
56
+ * @since 1.0.0-rc.0
57
+ * @private
58
+ */
59
+ export const isGlob = (entry: string): boolean => entry.charCodeAt(entry.length - 1) === STAR
60
+
61
+ /**
62
+ * The string prefix a pattern matches by: everything for `*` and `**`,
63
+ * `prefix/` for `prefix/**`, and `prefix` for `prefix*`.
64
+ *
65
+ * @since 1.0.0-rc.0
66
+ * @private
67
+ */
68
+ export const globPrefix = (glob: string): string =>
69
+ glob === "*" || glob === "**" ? "" : glob.endsWith("/**") ? glob.slice(0, -2) : glob.slice(0, -1)
70
+
71
+ /**
72
+ * Whether a pattern's prefix already holds a whole `.` or `..` segment. Every
73
+ * path under such a prefix carries that segment, so the pattern covers nothing
74
+ * and matches only an identical entry.
75
+ */
76
+ const inertPrefix = (prefix: string): boolean => hasDotSegment(prefix.slice(0, prefix.lastIndexOf("/") + 1))
77
+
78
+ /**
79
+ * The smallest string greater than every string that starts with `prefix`,
80
+ * or `undefined` when no such string exists. Strings starting with `prefix`
81
+ * are exactly those in `[prefix, successor(prefix))` in code-unit order, so a
82
+ * sorted list answers "which entries start with `prefix`" with two binary
83
+ * searches whose comparisons are native.
84
+ */
85
+ const successor = (prefix: string): string | undefined => {
86
+ let end = prefix.length
87
+ while (end > 0 && prefix.charCodeAt(end - 1) === 0xffff) end--
88
+ if (end === 0) return undefined
89
+ return prefix.slice(0, end - 1) + String.fromCharCode(prefix.charCodeAt(end - 1) + 1)
90
+ }
91
+
92
+ /** The first position in a sorted list whose entry is not less than `value`. */
93
+ const lowerBound = (sorted: ReadonlyArray<string>, value: string): number => {
94
+ let low = 0
95
+ let high = sorted.length
96
+ while (low < high) {
97
+ const middle = (low + high) >>> 1
98
+ if (sorted[middle]! < value) {
99
+ low = middle + 1
100
+ } else {
101
+ high = middle
102
+ }
103
+ }
104
+ return low
105
+ }
106
+
107
+ /** The first position in a sorted rank list whose rank is not less than `value`. */
108
+ const lowerBoundRank = (sorted: Int32Array, value: number): number => {
109
+ let low = 0
110
+ let high = sorted.length
111
+ while (low < high) {
112
+ const middle = (low + high) >>> 1
113
+ if (sorted[middle]! < value) {
114
+ low = middle + 1
115
+ } else {
116
+ high = middle
117
+ }
118
+ }
119
+ return low
120
+ }
121
+
122
+ /**
123
+ * The distinct paths of one or more declarations in code-unit order, with
124
+ * what matching needs to know about each: its rank, whether it carries a dot
125
+ * segment, whether it is a pattern that can cover anything, and for such a
126
+ * pattern the rank interval its prefix covers.
127
+ *
128
+ * @since 1.0.0-rc.0
129
+ * @private
130
+ */
131
+ export interface PathIndex {
132
+ readonly paths: ReadonlyArray<string>
133
+ readonly rank: ReadonlyMap<string, number>
134
+ readonly dotted: Uint8Array
135
+ readonly glob: Uint8Array
136
+ readonly low: Int32Array
137
+ readonly high: Int32Array
138
+ }
139
+
140
+ /**
141
+ * Indexes the union of the given path lists.
142
+ *
143
+ * @since 1.0.0-rc.0
144
+ * @private
145
+ */
146
+ export const indexPaths = (lists: Iterable<ReadonlyArray<string>>): PathIndex => {
147
+ const distinct = new Set<string>()
148
+ for (const list of lists) {
149
+ for (const path of list) distinct.add(path)
150
+ }
151
+ const paths = [...distinct].sort()
152
+ const rank = new Map<string, number>()
153
+ const count = paths.length
154
+ const dotted = new Uint8Array(count)
155
+ const glob = new Uint8Array(count)
156
+ const low = new Int32Array(count).fill(-1)
157
+ const high = new Int32Array(count).fill(-1)
158
+ for (let index = 0; index < count; index++) {
159
+ const path = paths[index]!
160
+ rank.set(path, index)
161
+ if (hasDotSegment(path)) dotted[index] = 1
162
+ if (!isGlob(path)) continue
163
+ const prefix = globPrefix(path)
164
+ if (inertPrefix(prefix)) continue
165
+ glob[index] = 1
166
+ low[index] = lowerBound(paths, prefix)
167
+ const next = successor(prefix)
168
+ high[index] = next === undefined ? count : lowerBound(paths, next)
169
+ }
170
+ return { paths, rank, dotted, glob, low, high }
171
+ }
172
+
173
+ /**
174
+ * One declaration's paths as ranks of a {@link PathIndex}: every path in
175
+ * ascending rank order, and the patterns whose prefix intervals are not
176
+ * contained in another of the declaration's patterns. Those intervals are
177
+ * disjoint and ascending, so enumerating them visits each covered rank once.
178
+ *
179
+ * @since 1.0.0-rc.0
180
+ * @private
181
+ */
182
+ export interface Ranked {
183
+ readonly ranks: Int32Array
184
+ readonly globs: Int32Array
185
+ }
186
+
187
+ /**
188
+ * Ranks a declaration's paths against an index that contains them.
189
+ *
190
+ * @since 1.0.0-rc.0
191
+ * @private
192
+ */
193
+ export const rankPaths = (index: PathIndex, paths: ReadonlyArray<string>): Ranked => {
194
+ const ranks = Int32Array.from(paths, (path) => index.rank.get(path)!).sort()
195
+ let distinct = 0
196
+ for (let position = 0; position < ranks.length; position++) {
197
+ if (position === 0 || ranks[position] !== ranks[position - 1]) ranks[distinct++] = ranks[position]!
198
+ }
199
+ const unique = ranks.subarray(0, distinct)
200
+ const candidates: Array<number> = []
201
+ for (const rank of unique) {
202
+ if (index.glob[rank] === 1 && index.low[rank]! < index.high[rank]!) candidates.push(rank)
203
+ }
204
+ candidates.sort((left, right) => index.low[left]! - index.low[right]! || index.high[right]! - index.high[left]!)
205
+ const globs: Array<number> = []
206
+ let coveredTo = -1
207
+ for (const rank of candidates) {
208
+ if (index.high[rank]! <= coveredTo) continue
209
+ globs.push(rank)
210
+ coveredTo = index.high[rank]!
211
+ }
212
+ return { ranks: unique, globs: Int32Array.from(globs) }
213
+ }
214
+
215
+ /** The ranks two ascending rank lists share, ascending. */
216
+ const shared = (a: Int32Array, b: Int32Array): Array<number> => {
217
+ const matches: Array<number> = []
218
+ let left = 0
219
+ let right = 0
220
+ while (left < a.length && right < b.length) {
221
+ const x = a[left]!
222
+ const y = b[right]!
223
+ if (x === y) {
224
+ matches.push(x)
225
+ left++
226
+ right++
227
+ } else if (x < y) {
228
+ left++
229
+ } else {
230
+ right++
231
+ }
232
+ }
233
+ return matches
234
+ }
235
+
236
+ /** Merges ascending, duplicate-free rank lists into one, dropping repeats. */
237
+ const union = (lists: ReadonlyArray<ReadonlyArray<number>>): Array<number> => {
238
+ const merged: Array<number> = []
239
+ const cursors = lists.map(() => 0)
240
+ while (true) {
241
+ let smallest = -1
242
+ for (let list = 0; list < lists.length; list++) {
243
+ const cursor = cursors[list]!
244
+ if (cursor < lists[list]!.length && (smallest === -1 || lists[list]![cursor]! < smallest)) {
245
+ smallest = lists[list]![cursor]!
246
+ }
247
+ }
248
+ if (smallest === -1) return merged
249
+ merged.push(smallest)
250
+ for (let list = 0; list < lists.length; list++) {
251
+ const cursor = cursors[list]!
252
+ if (cursor < lists[list]!.length && lists[list]![cursor] === smallest) cursors[list] = cursor + 1
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * The ranks of the paths two write declarations share, ascending: a path of
259
+ * `b` that `a` names or covers, and a path of `a` that `b` covers unless it is
260
+ * the same pattern or itself covers that pattern. This is the pairwise
261
+ * definition, exact paths matched through the merge of two sorted rank lists
262
+ * and each covering pattern enumerating the ranks of the other declaration
263
+ * inside its interval. Each of the three passes yields an ascending list, so
264
+ * the result is their merge and no sort is needed.
265
+ *
266
+ * @since 1.0.0-rc.0
267
+ * @private
268
+ */
269
+ export const overlapRanks = (index: PathIndex, a: Ranked, b: Ranked): Array<number> => {
270
+ const covered: Array<number> = []
271
+ for (const glob of a.globs) {
272
+ const high = index.high[glob]!
273
+ for (let position = lowerBoundRank(b.ranks, index.low[glob]!); position < b.ranks.length; position++) {
274
+ const rank = b.ranks[position]!
275
+ if (rank >= high) break
276
+ if (index.dotted[rank] === 0) covered.push(rank)
277
+ }
278
+ }
279
+ const covering: Array<number> = []
280
+ for (const glob of b.globs) {
281
+ const high = index.high[glob]!
282
+ const globDotted = index.dotted[glob] === 1
283
+ for (let position = lowerBoundRank(a.ranks, index.low[glob]!); position < a.ranks.length; position++) {
284
+ const rank = a.ranks[position]!
285
+ if (rank >= high) break
286
+ if (rank === glob || index.dotted[rank] === 1) continue
287
+ if (index.glob[rank] === 1 && !globDotted && index.low[rank]! <= glob && glob < index.high[rank]!) continue
288
+ covering.push(rank)
289
+ }
290
+ }
291
+ const exact = shared(a.ranks, b.ranks)
292
+ if (covered.length === 0 && covering.length === 0) return exact
293
+ return union([exact, covered, covering])
294
+ }
295
+
296
+ /** The first position in a sorted list whose entry is greater than `value`. */
297
+ const upperBound = (sorted: ReadonlyArray<string>, value: string): number => {
298
+ let low = 0
299
+ let high = sorted.length
300
+ while (low < high) {
301
+ const middle = (low + high) >>> 1
302
+ if (sorted[middle]! <= value) {
303
+ low = middle + 1
304
+ } else {
305
+ high = middle
306
+ }
307
+ }
308
+ return low
309
+ }
310
+
311
+ /**
312
+ * One envelope list prepared for repeated coverage checks: its distinct
313
+ * entries, matched exactly through a set, and the prefixes of its covering
314
+ * patterns with every pattern nested under another collapsed into the
315
+ * outermost. The kept prefixes are disjoint and sorted, so the only prefix
316
+ * that can cover a path is the greatest one not greater than the path, and
317
+ * one binary search finds it.
318
+ *
319
+ * @since 1.0.0-rc.0
320
+ * @private
321
+ */
322
+ export interface Prepared {
323
+ readonly exact: ReadonlySet<string>
324
+ readonly prefixes: ReadonlyArray<string>
325
+ }
326
+
327
+ /**
328
+ * Prepares one envelope list. Each entry is read a bounded number of times:
329
+ * once into the set, once to detect a pattern, and for a pattern once to take
330
+ * its prefix and once to test that prefix for a dot segment, plus the
331
+ * comparisons that sort the prefixes and one prefix comparison that collapses
332
+ * a nested one.
333
+ *
334
+ * @since 1.0.0-rc.0
335
+ * @private
336
+ */
337
+ export const prepare = (paths: ReadonlyArray<string>): Prepared => {
338
+ const exact = new Set(paths)
339
+ const candidates: Array<string> = []
340
+ for (const entry of exact) {
341
+ if (!isGlob(entry)) continue
342
+ const prefix = globPrefix(entry)
343
+ if (!inertPrefix(prefix)) candidates.push(prefix)
344
+ }
345
+ candidates.sort()
346
+ const prefixes: Array<string> = []
347
+ for (const prefix of candidates) {
348
+ // Strings that start with a prefix are contiguous in code-unit order, so
349
+ // every prefix nested under the last kept one follows it directly.
350
+ if (prefixes.length > 0 && prefix.startsWith(prefixes[prefixes.length - 1]!)) continue
351
+ prefixes.push(prefix)
352
+ }
353
+ return { exact, prefixes }
354
+ }
355
+
356
+ /**
357
+ * Whether a prepared list names or covers `path`. A path carrying a dot
358
+ * segment is never covered, so the path is scanned once for one and, when
359
+ * clean, looked up once and compared against at most one prefix.
360
+ *
361
+ * @since 1.0.0-rc.0
362
+ * @private
363
+ */
364
+ export const covered = (prepared: Prepared, path: string): boolean => {
365
+ if (hasDotSegment(path)) return false
366
+ if (prepared.exact.has(path)) return true
367
+ const position = upperBound(prepared.prefixes, path) - 1
368
+ return position >= 0 && path.startsWith(prepared.prefixes[position]!)
369
+ }
370
+
371
+ /**
372
+ * The fields of a declaration that narrowing reads. Structural, so this module
373
+ * needs nothing from the public module built on it.
374
+ *
375
+ * @since 1.0.0-rc.0
376
+ * @private
377
+ */
378
+ export interface DeclarationLike {
379
+ readonly reads: ReadonlyArray<string>
380
+ readonly writes: ReadonlyArray<string>
381
+ readonly mode: Effects.Mode
382
+ readonly tier?: Effects.Tier | undefined
383
+ }
384
+
385
+ /**
386
+ * An envelope prepared once for every step it encloses.
387
+ *
388
+ * @since 1.0.0-rc.0
389
+ * @private
390
+ */
391
+ export interface PreparedEnvelope {
392
+ readonly reads: Prepared
393
+ readonly writes: Prepared
394
+ readonly mode: Effects.Mode
395
+ readonly tier: Effects.Tier
396
+ }
397
+
398
+ /**
399
+ * The outcome of narrowing a step against a prepared envelope, shaped as the
400
+ * public `NarrowResult`.
401
+ *
402
+ * @since 1.0.0-rc.0
403
+ * @private
404
+ */
405
+ export type NarrowOutcome = Effects.NarrowResult
406
+
407
+ /**
408
+ * Prepares both lists of an envelope.
409
+ *
410
+ * @since 1.0.0-rc.0
411
+ * @private
412
+ */
413
+ export const prepareEnvelope = (envelope: DeclarationLike): PreparedEnvelope => ({
414
+ reads: prepare(envelope.reads),
415
+ writes: prepare(envelope.writes),
416
+ mode: envelope.mode,
417
+ tier: envelope.tier ?? "sealed"
418
+ })
419
+
420
+ const tierRank = { sealed: 0, compensable: 1, irreversible: 2 } as const
421
+
422
+ const outside = (prepared: Prepared, paths: ReadonlyArray<string>): Array<string> =>
423
+ paths.filter((path) => !covered(prepared, path))
424
+
425
+ /**
426
+ * Verifies that a step stays within a prepared envelope: every read and write
427
+ * path must be covered by the matching list, the mode may only tighten, and
428
+ * the tier may only narrow. Each step path costs one coverage check.
429
+ *
430
+ * @since 1.0.0-rc.0
431
+ * @private
432
+ */
433
+ export const narrowPrepared = (envelope: PreparedEnvelope, step: DeclarationLike): NarrowOutcome => {
434
+ const paths = [...new Set([...outside(envelope.reads, step.reads), ...outside(envelope.writes, step.writes)])].sort()
435
+ if (paths.length > 0) {
436
+ return { ok: false, code: "effect_outside_envelope", paths }
437
+ }
438
+ if (envelope.mode === "hermetic" && step.mode === "expected") {
439
+ return { ok: false, code: "effect_mode_widening", paths: [] }
440
+ }
441
+ if (tierRank[step.tier ?? "sealed"] > tierRank[envelope.tier]) {
442
+ return { ok: false, code: "effect_tier_widening", paths: [] }
443
+ }
444
+ return { ok: true }
445
+ }
446
+
447
+ /**
448
+ * Maximum length, in UTF-16 code units, of one effect path a graph build
449
+ * admits before it refuses the plan as too large. 4096 is `PATH_MAX`
450
+ * on Linux, the longest path a supported host can open, so no path that names
451
+ * a file is refused. Every per-character cost of a build is bounded by it:
452
+ * the one dot-segment scan each path gets, the comparisons that sort the
453
+ * distinct paths, and the comparisons that locate each pattern's prefix. The
454
+ * length is read before any character is, so an over-long path costs one
455
+ * property read.
456
+ *
457
+ * @category limits
458
+ * @since 1.0.0-rc.0
459
+ */
460
+ export const maximumPathLength = 4096
461
+
462
+ /**
463
+ * Maximum number of patterns, entries ending in `*`, one read list or one
464
+ * write list of an effect declaration may carry before a graph build refuses
465
+ * the plan as too large. A pattern never costs a match more than a
466
+ * literal path does: its prefix is located once by binary search, patterns
467
+ * nested under another collapse into the outermost before the paths they
468
+ * cover are enumerated, and every match after that is an integer comparison.
469
+ * What a pattern costs beyond a literal is that search, two binary searches
470
+ * of at most 16 comparisons each over a plan at the builder's plan-wide path
471
+ * ceiling,
472
+ * every comparison reading up to {@link maximumPathLength} code units.
473
+ * 128 keeps that term, 4,096 comparisons per list, below the sort that admits
474
+ * a per-declaration list of literal paths, while still letting one
475
+ * declaration name a subtree per package of a large monorepo. The count is
476
+ * read from the last character of each path as it is admitted, so a pattern
477
+ * past the limit costs one character read.
478
+ *
479
+ * @category limits
480
+ * @since 1.0.0-rc.0
481
+ */
482
+ export const maximumGlobs = 128
483
+
484
+ /**
485
+ * Copies one declaration's paths, refusing before the copy grows past
486
+ * `limit`. A real array is refused by its length before a member is read; any
487
+ * other iterable is copied one path at a time and refused as soon as it
488
+ * exceeds the limit, so a caller-assembled declaration cannot dodge the bound
489
+ * by hiding its size from `length`. Each path is then refused by its length
490
+ * before any character is read, and by the count of patterns the list has
491
+ * carried so far, so an over-long path or a pattern past the limit costs one
492
+ * property read.
493
+ *
494
+ * @private
495
+ * @since 1.0.0-rc.0
496
+ */
497
+ export const copyPaths = (
498
+ paths: ReadonlyArray<string>,
499
+ limit: number,
500
+ refuse: () => Error
501
+ ): Array<string> => {
502
+ const copy: Array<string> = []
503
+ let globs = 0
504
+ const admit = (path: string): void => {
505
+ if (path.length > maximumPathLength) throw refuse()
506
+ if (isGlob(path) && ++globs > maximumGlobs) throw refuse()
507
+ copy.push(path)
508
+ }
509
+ if (Array.isArray(paths)) {
510
+ const length = paths.length
511
+ if (length > limit) throw refuse()
512
+ for (let index = 0; index < length; index++) admit(paths[index])
513
+ return copy
514
+ }
515
+ for (const path of paths) {
516
+ if (copy.length >= limit) throw refuse()
517
+ admit(path)
518
+ }
519
+ return copy
520
+ }
521
+
522
+ /**
523
+ * Snapshots a declaration into graph-owned data, copying at most `limit`
524
+ * paths in total across its reads and writes.
525
+ *
526
+ * @private
527
+ * @since 1.0.0-rc.0
528
+ */
529
+ export const boundedEffects = (
530
+ declaration: Effects.Declaration,
531
+ limit: number,
532
+ refuse: () => Error
533
+ ): Effects.Declaration => {
534
+ const reads = copyPaths(declaration.reads, limit, refuse)
535
+ const writes = copyPaths(declaration.writes, limit - reads.length, refuse)
536
+ return {
537
+ reads,
538
+ writes,
539
+ mode: declaration.mode,
540
+ onConflict: declaration.onConflict,
541
+ ...(declaration.tier === undefined ? {} : { tier: declaration.tier })
542
+ }
543
+ }