@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,255 @@
1
+ /**
2
+ * The inert JSON mirror of a value: what `JSON.stringify` would see, as data.
3
+ *
4
+ * The AST stores payloads this way, the plan compiler stores key material this
5
+ * way, and a plan diff compares declarations this way, so all three share the
6
+ * one walk. A planned value is the only member with no JSON meaning of its
7
+ * own, and each caller decides what it becomes.
8
+ *
9
+ * @since 0.1.0
10
+ * @private
11
+ */
12
+
13
+ import { GraphBuildError } from "../GraphBuildError.ts"
14
+ import * as Planned from "../Planned.ts"
15
+
16
+ /**
17
+ * One container being cloned: the object walked, the clone being filled, and
18
+ * the member position the walk has reached. `keys` is `undefined` for an
19
+ * array, whose members are positional.
20
+ *
21
+ * @since 0.1.0
22
+ * @private
23
+ */
24
+ interface CloneFrame {
25
+ readonly source: Record<string, unknown> | ReadonlyArray<unknown>
26
+ readonly output: Record<string, unknown> | Array<unknown>
27
+ readonly descriptors: PropertyDescriptorMap
28
+ readonly keys: ReadonlyArray<string> | undefined
29
+ readonly path: ClonePath
30
+ /** Sources whose callable `toJSON` resolved to this container. */
31
+ readonly replaced: ReadonlyArray<object>
32
+ index: number
33
+ }
34
+
35
+ /**
36
+ * Where the walk stands, as a link to its parent rather than a copy of the
37
+ * whole path. Payload depth is bounded only by the caller, and copying the
38
+ * path at each member made a clone cost memory quadratic in that depth: a
39
+ * 20,000-deep payload held 20,000 live frames whose paths together ran to
40
+ * hundreds of millions of entries, several gigabytes, for a result that is
41
+ * linear. The path is read only to name a member in a refusal, so it stays a
42
+ * chain here and is flattened at the throw.
43
+ *
44
+ * `undefined` is the root, whose path is empty.
45
+ *
46
+ * @since 0.1.0
47
+ * @private
48
+ */
49
+ type ClonePath = { readonly parent: ClonePath; readonly key: string } | undefined
50
+
51
+ /** The chain read outwards, root first: the array the refusals report. */
52
+ const clonePath = (path: ClonePath): ReadonlyArray<string> => {
53
+ const keys: Array<string> = []
54
+ for (let current = path; current !== undefined; current = current.parent) keys.push(current.key)
55
+ return keys.reverse()
56
+ }
57
+
58
+ const payloadError = (at: ClonePath, reason: string): GraphBuildError => {
59
+ const path = clonePath(at)
60
+ return new GraphBuildError({
61
+ code: "invalid_payload",
62
+ node: "payload",
63
+ path,
64
+ message: `Plan payload at ${path.length === 0 ? "$" : `$.${path.join(".")}`} ${reason}`
65
+ })
66
+ }
67
+
68
+ const cyclicPayloadError = (at: ClonePath): GraphBuildError => {
69
+ const path = clonePath(at)
70
+ return new GraphBuildError({
71
+ code: "cyclic_payload",
72
+ node: "payload",
73
+ path,
74
+ message: `Plan payload at ${
75
+ path.length === 0 ? "$" : `$.${path.join(".")}`
76
+ } has a toJSON method that returns itself`
77
+ })
78
+ }
79
+
80
+ const inheritedDataProperty = (
81
+ value: object,
82
+ key: PropertyKey
83
+ ):
84
+ | { readonly kind: "data"; readonly value: unknown }
85
+ | { readonly kind: "accessor" }
86
+ | { readonly kind: "missing" } =>
87
+ {
88
+ let current: object | null = value
89
+ while (current !== null) {
90
+ const descriptor = Object.getOwnPropertyDescriptor(current, key)
91
+ if (descriptor !== undefined) {
92
+ return "value" in descriptor ? { kind: "data", value: descriptor.value } : { kind: "accessor" }
93
+ }
94
+ current = Object.getPrototypeOf(current) as object | null
95
+ }
96
+ return { kind: "missing" }
97
+ }
98
+
99
+ /**
100
+ * Clones the JSON mirror of the input, so {@link module:StepKey.content} over
101
+ * the clone and the input produce the same key and the clone holds only inert
102
+ * JSON data. The walk uses an explicit stack, memoizes shared references and
103
+ * cycles, honors callable `toJSON`, omits object members without a JSON
104
+ * representation, and writes `null` for those values in arrays.
105
+ *
106
+ * `toJSON` receives the key canonical serialization passes it: the member
107
+ * name, the array index, or `""` at the root. Its replacement is therefore
108
+ * memoized per source and key, since one shared source may serialize
109
+ * differently at two positions. A source met again inside its own
110
+ * replacement reuses that replacement, so a cycle through `toJSON` clones as
111
+ * a cycle whatever key it recurs at.
112
+ *
113
+ * A planned value becomes whatever `plannedValue` returns for it. Refusals
114
+ * throw a {@link GraphBuildError} naming the member's path.
115
+ *
116
+ * @since 0.1.0
117
+ * @private
118
+ */
119
+ export const jsonMirror = (
120
+ input: unknown,
121
+ plannedValue: (value: unknown, reference: Planned.Reference) => unknown
122
+ ): unknown => {
123
+ const missing = Symbol("missing JSON representation")
124
+ const seen = new WeakMap<object, unknown>()
125
+ const replacedAt = new WeakMap<object, Map<string, unknown>>()
126
+ const open = new Map<object, unknown>()
127
+ let result: unknown
128
+ const frames: Array<CloneFrame> = []
129
+ /** Resolves one member, opening a frame when it is an unseen container. */
130
+ const enter = (
131
+ initial: unknown,
132
+ place: (member: unknown | typeof missing) => void,
133
+ path: ClonePath
134
+ ): void => {
135
+ let current = initial
136
+ const key = path === undefined ? "" : path.key
137
+ const replacements: Array<object> = []
138
+ const resolving = new WeakSet<object>()
139
+ const finish = (member: unknown | typeof missing): void => {
140
+ for (const replacement of replacements) {
141
+ let byKey = replacedAt.get(replacement)
142
+ if (byKey === undefined) replacedAt.set(replacement, byKey = new Map())
143
+ byKey.set(key, member)
144
+ }
145
+ place(member)
146
+ }
147
+ while (true) {
148
+ const reference = Planned.reference(current)
149
+ if (reference !== undefined) {
150
+ finish(plannedValue(current, reference))
151
+ return
152
+ }
153
+ const kind = typeof current
154
+ if (current === null || (kind !== "object" && kind !== "function")) {
155
+ finish(kind === "undefined" || kind === "symbol" ? missing : current)
156
+ return
157
+ }
158
+ const source = current as object
159
+ if (seen.has(source)) {
160
+ finish(seen.get(source))
161
+ return
162
+ }
163
+ if (resolving.has(source)) {
164
+ throw cyclicPayloadError(path)
165
+ }
166
+ const toJSON = inheritedDataProperty(source, "toJSON")
167
+ if (toJSON.kind === "accessor") {
168
+ throw payloadError(path, "has an accessor-backed toJSON member")
169
+ }
170
+ if (toJSON.kind === "data" && typeof toJSON.value === "function") {
171
+ const byKey = replacedAt.get(source)
172
+ if (byKey !== undefined && byKey.has(key)) {
173
+ finish(byKey.get(key))
174
+ return
175
+ }
176
+ if (open.has(source)) {
177
+ finish(open.get(source))
178
+ return
179
+ }
180
+ resolving.add(source)
181
+ replacements.push(source)
182
+ current = Reflect.apply(toJSON.value, source, [key])
183
+ continue
184
+ }
185
+ if (kind === "function") {
186
+ seen.set(source, missing)
187
+ finish(missing)
188
+ return
189
+ }
190
+ const prototype = Object.getPrototypeOf(source)
191
+ if (!Array.isArray(source) && prototype !== Object.prototype && prototype !== null) {
192
+ throw payloadError(path, "has an unsupported prototype and no data-valued toJSON method")
193
+ }
194
+ const container = current as Record<string, unknown> | ReadonlyArray<unknown>
195
+ const descriptors = Object.getOwnPropertyDescriptors(container) as PropertyDescriptorMap
196
+ const output: Record<string, unknown> | Array<unknown> = Array.isArray(container)
197
+ ? []
198
+ : Object.create(null) as Record<string, unknown>
199
+ seen.set(container, output)
200
+ finish(output)
201
+ frames.push({
202
+ source: container,
203
+ output,
204
+ descriptors,
205
+ keys: Array.isArray(container)
206
+ ? undefined
207
+ : Reflect.ownKeys(descriptors).filter((key): key is string =>
208
+ typeof key === "string" && descriptors[key]!.enumerable === true
209
+ ),
210
+ path,
211
+ replaced: replacements,
212
+ index: 0
213
+ })
214
+ for (const replacement of replacements) open.set(replacement, output)
215
+ return
216
+ }
217
+ }
218
+ enter(input, (member) => {
219
+ result = member === missing ? undefined : member
220
+ }, undefined)
221
+ while (frames.length > 0) {
222
+ const frame = frames[frames.length - 1]!
223
+ if (frame.index >= (frame.keys ?? frame.source as ReadonlyArray<unknown>).length) {
224
+ frames.pop()
225
+ for (const replacement of frame.replaced) open.delete(replacement)
226
+ continue
227
+ }
228
+ const position = frame.index
229
+ frame.index = position + 1
230
+ if (frame.keys === undefined) {
231
+ const members = frame.output as Array<unknown>
232
+ const descriptor = frame.descriptors[String(position)]
233
+ if (descriptor !== undefined && !("value" in descriptor)) {
234
+ throw payloadError({ parent: frame.path, key: String(position) }, "is an accessor")
235
+ }
236
+ enter(descriptor === undefined ? undefined : descriptor.value, (member) => {
237
+ members.push(member === missing ? null : member)
238
+ }, { parent: frame.path, key: String(position) })
239
+ } else {
240
+ const key = frame.keys[position]!
241
+ const descriptor = frame.descriptors[key]!
242
+ if (!("value" in descriptor)) throw payloadError({ parent: frame.path, key }, "is an accessor")
243
+ enter(descriptor.value, (member) => {
244
+ if (member === missing) return
245
+ Object.defineProperty(frame.output, key, {
246
+ configurable: true,
247
+ enumerable: true,
248
+ value: member,
249
+ writable: true
250
+ })
251
+ }, { parent: frame.path, key })
252
+ }
253
+ }
254
+ return result
255
+ }