@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.
- package/CHANGELOG.md +183 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/dist/cjs/CachePolicy.d.ts +131 -0
- package/dist/cjs/CachePolicy.d.ts.map +1 -0
- package/dist/cjs/CachePolicy.js +50 -0
- package/dist/cjs/CachePolicy.js.map +7 -0
- package/dist/cjs/Effects.d.ts +240 -0
- package/dist/cjs/Effects.d.ts.map +1 -0
- package/dist/cjs/Effects.js +76 -0
- package/dist/cjs/Effects.js.map +7 -0
- package/dist/cjs/FileSet.d.ts +288 -0
- package/dist/cjs/FileSet.d.ts.map +1 -0
- package/dist/cjs/FileSet.js +191 -0
- package/dist/cjs/FileSet.js.map +7 -0
- package/dist/cjs/GraphBuildError.d.ts +122 -0
- package/dist/cjs/GraphBuildError.d.ts.map +1 -0
- package/dist/cjs/GraphBuildError.js +71 -0
- package/dist/cjs/GraphBuildError.js.map +7 -0
- package/dist/cjs/KeyMaterial.d.ts +102 -0
- package/dist/cjs/KeyMaterial.d.ts.map +1 -0
- package/dist/cjs/KeyMaterial.js +64 -0
- package/dist/cjs/KeyMaterial.js.map +7 -0
- package/dist/cjs/Node.d.ts +606 -0
- package/dist/cjs/Node.d.ts.map +1 -0
- package/dist/cjs/Node.js +207 -0
- package/dist/cjs/Node.js.map +7 -0
- package/dist/cjs/Placement.d.ts +117 -0
- package/dist/cjs/Placement.d.ts.map +1 -0
- package/dist/cjs/Placement.js +46 -0
- package/dist/cjs/Placement.js.map +7 -0
- package/dist/cjs/Plan.d.ts +440 -0
- package/dist/cjs/Plan.d.ts.map +1 -0
- package/dist/cjs/Plan.js +508 -0
- package/dist/cjs/Plan.js.map +7 -0
- package/dist/cjs/PlanDiff.d.ts +58 -0
- package/dist/cjs/PlanDiff.d.ts.map +1 -0
- package/dist/cjs/PlanDiff.js +124 -0
- package/dist/cjs/PlanDiff.js.map +7 -0
- package/dist/cjs/Planned.d.ts +121 -0
- package/dist/cjs/Planned.d.ts.map +1 -0
- package/dist/cjs/Planned.js +72 -0
- package/dist/cjs/Planned.js.map +7 -0
- package/dist/cjs/Repetition.d.ts +31 -0
- package/dist/cjs/Repetition.d.ts.map +1 -0
- package/dist/cjs/Repetition.js +36 -0
- package/dist/cjs/Repetition.js.map +7 -0
- package/dist/cjs/Scheduling.d.ts +89 -0
- package/dist/cjs/Scheduling.d.ts.map +1 -0
- package/dist/cjs/Scheduling.js +76 -0
- package/dist/cjs/Scheduling.js.map +7 -0
- package/dist/cjs/StepKey.d.ts +333 -0
- package/dist/cjs/StepKey.d.ts.map +1 -0
- package/dist/cjs/StepKey.js +311 -0
- package/dist/cjs/StepKey.js.map +7 -0
- package/dist/cjs/index.d.ts +91 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +59 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ConflictAnnotation.d.ts +59 -0
- package/dist/cjs/internal/ConflictAnnotation.d.ts.map +1 -0
- package/dist/cjs/internal/ConflictAnnotation.js +247 -0
- package/dist/cjs/internal/ConflictAnnotation.js.map +7 -0
- package/dist/cjs/internal/EffectCandidates.d.ts +19 -0
- package/dist/cjs/internal/EffectCandidates.d.ts.map +1 -0
- package/dist/cjs/internal/EffectCandidates.js +92 -0
- package/dist/cjs/internal/EffectCandidates.js.map +7 -0
- package/dist/cjs/internal/JsonMirror.d.ts +34 -0
- package/dist/cjs/internal/JsonMirror.d.ts.map +1 -0
- package/dist/cjs/internal/JsonMirror.js +195 -0
- package/dist/cjs/internal/JsonMirror.js.map +7 -0
- package/dist/cjs/internal/effects.d.ts +245 -0
- package/dist/cjs/internal/effects.d.ts.map +1 -0
- package/dist/cjs/internal/effects.js +290 -0
- package/dist/cjs/internal/effects.js.map +7 -0
- package/dist/cjs/internal/node.d.ts +526 -0
- package/dist/cjs/internal/node.d.ts.map +1 -0
- package/dist/cjs/internal/node.js +312 -0
- package/dist/cjs/internal/node.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/PlanFixtures.d.ts +127 -0
- package/dist/cjs/test/PlanFixtures.d.ts.map +1 -0
- package/dist/cjs/test/PlanFixtures.js +59 -0
- package/dist/cjs/test/PlanFixtures.js.map +7 -0
- package/dist/esm/CachePolicy.d.ts +131 -0
- package/dist/esm/CachePolicy.d.ts.map +1 -0
- package/dist/esm/CachePolicy.js +117 -0
- package/dist/esm/CachePolicy.js.map +1 -0
- package/dist/esm/Effects.d.ts +240 -0
- package/dist/esm/Effects.d.ts.map +1 -0
- package/dist/esm/Effects.js +167 -0
- package/dist/esm/Effects.js.map +1 -0
- package/dist/esm/FileSet.d.ts +288 -0
- package/dist/esm/FileSet.d.ts.map +1 -0
- package/dist/esm/FileSet.js +324 -0
- package/dist/esm/FileSet.js.map +1 -0
- package/dist/esm/GraphBuildError.d.ts +122 -0
- package/dist/esm/GraphBuildError.d.ts.map +1 -0
- package/dist/esm/GraphBuildError.js +146 -0
- package/dist/esm/GraphBuildError.js.map +1 -0
- package/dist/esm/KeyMaterial.d.ts +102 -0
- package/dist/esm/KeyMaterial.d.ts.map +1 -0
- package/dist/esm/KeyMaterial.js +85 -0
- package/dist/esm/KeyMaterial.js.map +1 -0
- package/dist/esm/Node.d.ts +606 -0
- package/dist/esm/Node.d.ts.map +1 -0
- package/dist/esm/Node.js +521 -0
- package/dist/esm/Node.js.map +1 -0
- package/dist/esm/Placement.d.ts +117 -0
- package/dist/esm/Placement.d.ts.map +1 -0
- package/dist/esm/Placement.js +65 -0
- package/dist/esm/Placement.js.map +1 -0
- package/dist/esm/Plan.d.ts +440 -0
- package/dist/esm/Plan.d.ts.map +1 -0
- package/dist/esm/Plan.js +673 -0
- package/dist/esm/Plan.js.map +1 -0
- package/dist/esm/PlanDiff.d.ts +58 -0
- package/dist/esm/PlanDiff.d.ts.map +1 -0
- package/dist/esm/PlanDiff.js +159 -0
- package/dist/esm/PlanDiff.js.map +1 -0
- package/dist/esm/Planned.d.ts +121 -0
- package/dist/esm/Planned.d.ts.map +1 -0
- package/dist/esm/Planned.js +147 -0
- package/dist/esm/Planned.js.map +1 -0
- package/dist/esm/Repetition.d.ts +31 -0
- package/dist/esm/Repetition.d.ts.map +1 -0
- package/dist/esm/Repetition.js +24 -0
- package/dist/esm/Repetition.js.map +1 -0
- package/dist/esm/Scheduling.d.ts +89 -0
- package/dist/esm/Scheduling.d.ts.map +1 -0
- package/dist/esm/Scheduling.js +82 -0
- package/dist/esm/Scheduling.js.map +1 -0
- package/dist/esm/StepKey.d.ts +333 -0
- package/dist/esm/StepKey.d.ts.map +1 -0
- package/dist/esm/StepKey.js +478 -0
- package/dist/esm/StepKey.js.map +1 -0
- package/dist/esm/index.d.ts +91 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +91 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ConflictAnnotation.d.ts +59 -0
- package/dist/esm/internal/ConflictAnnotation.d.ts.map +1 -0
- package/dist/esm/internal/ConflictAnnotation.js +332 -0
- package/dist/esm/internal/ConflictAnnotation.js.map +1 -0
- package/dist/esm/internal/EffectCandidates.d.ts +19 -0
- package/dist/esm/internal/EffectCandidates.d.ts.map +1 -0
- package/dist/esm/internal/EffectCandidates.js +86 -0
- package/dist/esm/internal/EffectCandidates.js.map +1 -0
- package/dist/esm/internal/JsonMirror.d.ts +34 -0
- package/dist/esm/internal/JsonMirror.d.ts.map +1 -0
- package/dist/esm/internal/JsonMirror.js +204 -0
- package/dist/esm/internal/JsonMirror.js.map +1 -0
- package/dist/esm/internal/effects.d.ts +245 -0
- package/dist/esm/internal/effects.d.ts.map +1 -0
- package/dist/esm/internal/effects.js +459 -0
- package/dist/esm/internal/effects.js.map +1 -0
- package/dist/esm/internal/node.d.ts +526 -0
- package/dist/esm/internal/node.d.ts.map +1 -0
- package/dist/esm/internal/node.js +564 -0
- package/dist/esm/internal/node.js.map +1 -0
- package/dist/esm/test/PlanFixtures.d.ts +127 -0
- package/dist/esm/test/PlanFixtures.d.ts.map +1 -0
- package/dist/esm/test/PlanFixtures.js +60 -0
- package/dist/esm/test/PlanFixtures.js.map +1 -0
- package/docs/README.md +165 -0
- package/docs/api.md +1475 -0
- package/docs/concepts/authoring-ast.md +203 -0
- package/docs/concepts/effects-and-conflicts.md +135 -0
- package/docs/concepts/plan-value.md +168 -0
- package/docs/concepts/step-keys.md +179 -0
- package/docs/guides/author-a-node-graph.md +182 -0
- package/docs/guides/compile-a-plan.md +146 -0
- package/docs/guides/declare-file-effects.md +143 -0
- package/docs/guides/diff-two-plans.md +101 -0
- package/docs/installation.md +71 -0
- package/docs/quickstart.md +128 -0
- package/docs/testing.md +73 -0
- package/docs/troubleshooting.md +314 -0
- package/package.json +254 -3
- package/src/CachePolicy.ts +143 -0
- package/src/Effects.ts +284 -0
- package/src/FileSet.ts +411 -0
- package/src/GraphBuildError.ts +158 -0
- package/src/KeyMaterial.ts +106 -0
- package/src/Node.ts +871 -0
- package/src/Placement.ts +99 -0
- package/src/Plan.ts +868 -0
- package/src/PlanDiff.ts +199 -0
- package/src/Planned.ts +208 -0
- package/src/Repetition.ts +33 -0
- package/src/Scheduling.ts +154 -0
- package/src/StepKey.ts +710 -0
- package/src/index.ts +103 -0
- package/src/internal/ConflictAnnotation.ts +374 -0
- package/src/internal/EffectCandidates.ts +89 -0
- package/src/internal/JsonMirror.ts +255 -0
- package/src/internal/effects.ts +543 -0
- package/src/internal/node.ts +858 -0
- 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
|
+
}
|