@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
package/src/FileSet.ts ADDED
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Static filesystem declaration vocabulary shared by planning and execution.
3
+ *
4
+ * Declared paths use `/` as their canonical separator and normalize Unicode
5
+ * to NFC. Workspace-relative paths reject C0 controls (U+0000 through U+001F)
6
+ * and DEL (U+007F), which name nothing a supported filesystem can open. C1
7
+ * controls (U+0080 through U+009F) remain accepted because those bytes are
8
+ * legal in a POSIX file name.
9
+ *
10
+ * @since 0.1.0
11
+ */
12
+
13
+ import * as Schema from "effect/Schema"
14
+
15
+ /**
16
+ * How strictly a declared file set is enforced at the boundary.
17
+ *
18
+ * `hard` rejects undeclared access. `expected` records access for validation
19
+ * after execution without requiring the sandbox to reject it immediately.
20
+ *
21
+ * This is the ONE spelling of the pair. `Plan.NodeEffects.boundaryMode` is this
22
+ * schema and so is `@smthrs/flow`'s `Action.BoundaryMode`; the literal used to
23
+ * be written out in all three places.
24
+ *
25
+ * @category schemas
26
+ * @since 1.0.0-rc.0
27
+ */
28
+ export const BoundaryMode = Schema.Literals(["hard", "expected"])
29
+
30
+ /**
31
+ * The value form of {@link BoundaryMode}.
32
+ *
33
+ * @category models
34
+ * @since 1.0.0-rc.0
35
+ */
36
+ export type BoundaryMode = typeof BoundaryMode.Type
37
+
38
+ /**
39
+ * The canonical spelling of a declared path or pattern: every separator is
40
+ * `/`, and Unicode is normalized to NFC.
41
+ *
42
+ * {@link workspaceRelative} accepts a backslash as a separator, so
43
+ * `dist\same.js` and `dist/same.js` are two spellings of ONE workspace path.
44
+ * Every exact-path comparison in this module goes through this form, because
45
+ * comparing the raw spellings would let the separator alias defeat overlap
46
+ * detection exactly as `.` segments and empty segments would.
47
+ *
48
+ * @category accessors
49
+ * @since 0.1.0
50
+ * @slop
51
+ */
52
+ export const canonical = (path: string): string => path.replaceAll("\\", "/").normalize("NFC")
53
+
54
+ /**
55
+ * The {@link canonical} spelling with letter case folded away.
56
+ *
57
+ * A case-insensitive filesystem (default macOS APFS, Windows NTFS) opens
58
+ * `src/A.ts` and `src/a.ts` as one file, so static overlap compares this form:
59
+ * two declarations that differ only in case must serialize. Folding goes
60
+ * through upper case because that mapping is context-free, so it folds each
61
+ * character alone and commutes with splitting a path into segments.
62
+ *
63
+ * @category accessors
64
+ * @since 1.0.0-rc.0
65
+ */
66
+ export const folded = (path: string): string => canonical(path).toUpperCase().normalize("NFC")
67
+
68
+ /** Workspace-relative glob pattern.
69
+ * @category schemas
70
+ * @since 0.1.0
71
+ * @slop
72
+ */
73
+ export const Pattern = Schema.NonEmptyString.check(
74
+ Schema.makeFilter(
75
+ (pattern) => workspaceRelative(pattern) || "file patterns must be workspace-relative and cannot traverse upward",
76
+ { title: "workspaceRelativePattern" }
77
+ )
78
+ )
79
+
80
+ /**
81
+ * Whether a declared path or pattern stays inside the workspace and names it
82
+ * one way only. Refuses absolute paths (POSIX and drive-letter), upward
83
+ * traversal, and the aliasing forms — `.` segments, empty segments — that
84
+ * would let two spellings of one file defeat exact-string overlap detection.
85
+ *
86
+ * @category predicates
87
+ * @since 0.1.0
88
+ * @slop
89
+ */
90
+ export const workspaceRelative = (pattern: string): boolean => {
91
+ for (let index = 0; index < pattern.length; index++) {
92
+ const code = pattern.charCodeAt(index)
93
+ if (code <= 0x1f || code === 0x7f) return false
94
+ }
95
+ if (pattern.startsWith("/")) return false
96
+ const segments = canonical(pattern).split("/")
97
+ // `C:foo` is drive-relative on Windows, so any drive-letter prefix escapes.
98
+ if (/^[A-Za-z]:/.test(segments[0]!) || segments[0]!.endsWith(":")) return false
99
+ return segments.every((segment) => segment !== ".." && segment !== "." && segment !== "")
100
+ }
101
+
102
+ /** Bazel-style file glob.
103
+ * @category schemas
104
+ * @since 0.1.0
105
+ * @slop
106
+ */
107
+ export const Glob = Schema.TaggedStruct("Glob", {
108
+ include: Schema.NonEmptyArray(Pattern),
109
+ exclude: Schema.optional(Schema.Array(Pattern))
110
+ })
111
+
112
+ /** Bazel-style file glob.
113
+ * @category models
114
+ * @since 0.1.0
115
+ * @slop
116
+ */
117
+ export type Glob = typeof Glob.Type
118
+
119
+ /** A directory output captured and replayed as one tree artifact.
120
+ * @category schemas
121
+ * @since 0.1.0
122
+ * @slop
123
+ */
124
+ export const TreeArtifact = Schema.TaggedStruct("TreeArtifact", {
125
+ path: Pattern
126
+ })
127
+
128
+ /** A directory output captured and replayed as one tree artifact.
129
+ * @category models
130
+ * @since 0.1.0
131
+ * @slop
132
+ */
133
+ export type TreeArtifact = typeof TreeArtifact.Type
134
+
135
+ /** One leaf of a named filegroup.
136
+ * @category schemas
137
+ * @since 0.1.0
138
+ * @slop
139
+ */
140
+ export const Entry = Schema.Union([Pattern, Glob, TreeArtifact])
141
+
142
+ /** One leaf of a named filegroup.
143
+ * @category models
144
+ * @since 0.1.0
145
+ * @slop
146
+ */
147
+ export type Entry = typeof Entry.Type
148
+
149
+ /** A declaration valid in a read set.
150
+ * @category schemas
151
+ * @since 0.1.0
152
+ * @slop
153
+ */
154
+ export const ReadEntry = Schema.Union([Pattern, Glob])
155
+
156
+ /** A declaration valid in a read set.
157
+ * @category models
158
+ * @since 0.1.0
159
+ * @slop
160
+ */
161
+ export type ReadEntry = typeof ReadEntry.Type
162
+
163
+ /** A named reusable collection expanded before measurement.
164
+ * @category schemas
165
+ * @since 0.1.0
166
+ * @slop
167
+ */
168
+ export const Filegroup = Schema.TaggedStruct("Filegroup", {
169
+ name: Schema.NonEmptyString,
170
+ entries: Schema.Array(Entry)
171
+ })
172
+
173
+ /** A named reusable collection expanded before measurement.
174
+ * @category models
175
+ * @since 0.1.0
176
+ * @slop
177
+ */
178
+ export type Filegroup = typeof Filegroup.Type
179
+
180
+ /** A named read-only collection.
181
+ * @category schemas
182
+ * @since 0.1.0
183
+ * @slop
184
+ */
185
+ export const ReadFilegroup = Schema.TaggedStruct("Filegroup", {
186
+ name: Schema.NonEmptyString,
187
+ entries: Schema.Array(ReadEntry)
188
+ })
189
+
190
+ /** A named read-only collection.
191
+ * @category models
192
+ * @since 0.1.0
193
+ * @slop
194
+ */
195
+ export type ReadFilegroup = typeof ReadFilegroup.Type
196
+
197
+ /** Any declaration accepted by a plan effect set.
198
+ * @category schemas
199
+ * @since 0.1.0
200
+ * @slop
201
+ */
202
+ export const Declaration = Schema.Union([Entry, Filegroup])
203
+
204
+ /** Any declaration accepted by a plan effect set.
205
+ * @category models
206
+ * @since 0.1.0
207
+ * @slop
208
+ */
209
+ export type Declaration = typeof Declaration.Type
210
+
211
+ /** Any declaration accepted by a plan read set.
212
+ * @category schemas
213
+ * @since 0.1.0
214
+ * @slop
215
+ */
216
+ export const ReadDeclaration = Schema.Union([ReadEntry, ReadFilegroup])
217
+
218
+ /** Any declaration accepted by a plan read set.
219
+ * @category models
220
+ * @since 0.1.0
221
+ * @slop
222
+ */
223
+ export type ReadDeclaration = typeof ReadDeclaration.Type
224
+
225
+ /** Creates a named filegroup.
226
+ * @category constructors
227
+ * @since 0.1.0
228
+ * @slop
229
+ */
230
+ export const makeFilegroup = (name: string, entries: ReadonlyArray<Entry>): Filegroup => ({
231
+ _tag: "Filegroup",
232
+ name,
233
+ entries
234
+ })
235
+
236
+ /** Expands filegroups deterministically, preserving declaration order.
237
+ * @category accessors
238
+ * @since 0.1.0
239
+ * @slop
240
+ */
241
+ export const expand = (declarations: ReadonlyArray<Declaration>): ReadonlyArray<Entry> =>
242
+ declarations.flatMap((declaration) =>
243
+ typeof declaration !== "string" && declaration._tag === "Filegroup" ? declaration.entries : [declaration]
244
+ )
245
+
246
+ /** Expands read filegroups deterministically, preserving declaration order.
247
+ * @category accessors
248
+ * @since 0.1.0
249
+ * @slop
250
+ */
251
+ export const expandReads = (declarations: ReadonlyArray<ReadDeclaration>): ReadonlyArray<ReadEntry> =>
252
+ declarations.flatMap((declaration) =>
253
+ typeof declaration !== "string" && declaration._tag === "Filegroup" ? declaration.entries : [declaration]
254
+ )
255
+
256
+ /** Whether a value is a glob declaration.
257
+ * @category guards
258
+ * @since 0.1.0
259
+ * @slop
260
+ */
261
+ export const isGlob = (value: unknown): value is Glob =>
262
+ typeof value === "object" && value !== null && "_tag" in value && value._tag === "Glob"
263
+
264
+ /** Whether a value is a tree artifact declaration.
265
+ * @category guards
266
+ * @since 0.1.0
267
+ * @slop
268
+ */
269
+ export const isTreeArtifact = (value: unknown): value is TreeArtifact =>
270
+ typeof value === "object" && value !== null && "_tag" in value && value._tag === "TreeArtifact"
271
+
272
+ /** KMP searches a literal without rescanning a long near-matching prefix. */
273
+ const literalSearch = (literal: string) => {
274
+ const fallback = new Uint32Array(literal.length)
275
+ let matched = 0
276
+ for (let index = 1; index < literal.length; index++) {
277
+ while (matched > 0 && literal[index] !== literal[matched]) matched = fallback[matched - 1]!
278
+ if (literal[index] === literal[matched]) matched++
279
+ fallback[index] = matched
280
+ }
281
+ return (path: string, start: number, end: number): number => {
282
+ let matched = 0
283
+ for (let index = start; index < end; index++) {
284
+ while (matched > 0 && path[index] !== literal[matched]) matched = fallback[matched - 1]!
285
+ if (path[index] === literal[matched]) matched++
286
+ if (matched === literal.length) return index + 1
287
+ }
288
+ return -1
289
+ }
290
+ }
291
+
292
+ /** Anchored ends and disjoint literal searches make each segment match linear. */
293
+ const segmentMatcher = (segment: string): (path: string) => boolean => {
294
+ const chunks = segment.split(/\*+/)
295
+ const prefix = chunks.shift()!
296
+ if (chunks.length === 0) return (path) => path === prefix
297
+ const suffix = chunks.pop()!
298
+ const searches = chunks.map(literalSearch)
299
+ return (path) => {
300
+ const end = path.length - suffix.length
301
+ if (prefix.length > end || !path.startsWith(prefix) || !path.endsWith(suffix)) return false
302
+ let position = prefix.length
303
+ for (const search of searches) {
304
+ position = search(path, position, end)
305
+ if (position === -1) return false
306
+ }
307
+ return true
308
+ }
309
+ }
310
+
311
+ const patternMatchers = new Map<string, (path: string) => boolean>()
312
+
313
+ /** Compile once per canonical spelling; recursive segments use no backtracking. */
314
+ const patternMatcher = (pattern: string): (path: string) => boolean => {
315
+ const key = canonical(pattern)
316
+ const cached = patternMatchers.get(key)
317
+ if (cached !== undefined) return cached
318
+ const segments = key.split("/").map((segment) => segment === "**" ? undefined : segmentMatcher(segment))
319
+ const matcher = (path: string): boolean => {
320
+ const parts = path.split("/")
321
+ // Suffix membership: next[j] says the remaining pattern matches parts[j..].
322
+ // Each pattern/path segment pair is visited once, even with repeated **.
323
+ let next = new Uint8Array(parts.length + 1)
324
+ next[parts.length] = 1
325
+ for (let index = segments.length - 1; index >= 0; index--) {
326
+ const segment = segments[index]
327
+ const current = new Uint8Array(parts.length + 1)
328
+ for (let part = parts.length - 1; part >= 0; part--) {
329
+ current[part] = segment === undefined
330
+ // A trailing ** accepts every remaining code unit. It still needs
331
+ // the separator preceding it: src/** does not match bare src.
332
+ ? Number(
333
+ index === segments.length - 1 || next[part] === 1 ||
334
+ (parts[part] !== "" && current[part + 1] === 1)
335
+ )
336
+ : Number(next[part + 1] === 1 && segment(parts[part]!))
337
+ }
338
+ next = current
339
+ }
340
+ return next[0] === 1
341
+ }
342
+ patternMatchers.set(key, matcher)
343
+ return matcher
344
+ }
345
+
346
+ /** Tests one workspace-relative path against one Bazel-style pattern.
347
+ * @category predicates
348
+ * @since 0.1.0
349
+ * @slop
350
+ */
351
+ export const matchesPattern = (pattern: string, path: string): boolean => patternMatcher(pattern)(path)
352
+
353
+ /** Tests one file path against a glob, including exclusions.
354
+ * @category predicates
355
+ * @since 0.1.0
356
+ * @slop
357
+ */
358
+ export const matchesGlob = (glob: Glob, path: string): boolean =>
359
+ glob.include.some((pattern) => matchesPattern(pattern, path)) &&
360
+ !(glob.exclude ?? []).some((pattern) => matchesPattern(pattern, path))
361
+
362
+ const beneath = (tree: string, path: string): boolean => {
363
+ const root = folded(tree)
364
+ const inside = folded(path)
365
+ return inside === root || inside.startsWith(`${root}/`)
366
+ }
367
+
368
+ /** {@link matchesGlob} over {@link folded} spellings of the glob and the path. */
369
+ const matchesGlobFolded = (glob: Glob, path: string): boolean => {
370
+ const inside = folded(path)
371
+ return glob.include.some((pattern) => matchesPattern(folded(pattern), inside)) &&
372
+ !(glob.exclude ?? []).some((pattern) => matchesPattern(folded(pattern), inside))
373
+ }
374
+
375
+ /**
376
+ * Conservative static overlap. `true` may over-serialize; `false` proves that
377
+ * no path can belong to both declarations.
378
+ *
379
+ * A plain string is a literal path, even when it contains `*` or `**`, as it
380
+ * is at execution (sandbox admission, capture, removal, and replay); wildcard
381
+ * matching belongs to a Glob.
382
+ *
383
+ * Exact paths and tree roots compare in their
384
+ * {@link folded} form, so separator aliases, canonically equivalent Unicode
385
+ * spellings, and spellings that differ only in letter case overlap. A Glob
386
+ * against an exact path matches both its `include` and `exclude` patterns in
387
+ * folded form. {@link matchesGlob} itself stays case-sensitive and tests the path bytes it
388
+ * is handed; its property suite pins a backslash inside a path segment as
389
+ * literal text, so canonicalizing a measured path before matching is its
390
+ * caller's decision, not this module's.
391
+ *
392
+ * @category predicates
393
+ * @since 0.1.0
394
+ * @slop
395
+ */
396
+ export const overlaps = (left: Entry, right: Entry): boolean => {
397
+ if (typeof left === "string" && typeof right === "string") return folded(left) === folded(right)
398
+ if (typeof left === "string" && isGlob(right)) return matchesGlobFolded(right, left)
399
+ if (isGlob(left) && typeof right === "string") return matchesGlobFolded(left, right)
400
+ if (isGlob(left) && isGlob(right)) return true
401
+ if (isTreeArtifact(left) && typeof right === "string") return beneath(left.path, right)
402
+ if (typeof left === "string" && isTreeArtifact(right)) return beneath(right.path, left)
403
+ if (isTreeArtifact(left) && isTreeArtifact(right)) {
404
+ return beneath(left.path, right.path) || beneath(right.path, left.path)
405
+ }
406
+ if (isTreeArtifact(left) && isGlob(right)) return true
407
+ /* v8 ignore else -- the closed Entry union leaves only Glob/TreeArtifact here */
408
+ if (isGlob(left) && isTreeArtifact(right)) return overlaps(right, left)
409
+ /* v8 ignore next -- Entry's closed union makes every pair return above */
410
+ return true
411
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The refusals a plan-time build raises instead of producing a wrong plan.
3
+ *
4
+ * Every one of these fails LOUDLY at build time: a planned value that was computed on must not
5
+ * bake `NaN` into a plan, an arm that returned something other than a node must
6
+ * not silently vanish from the topology, and a recursive `.call()` must direct
7
+ * the author to `.to()` or `.child()` rather than expanding forever. Each
8
+ * failure therefore names the site (`node` plus the recorded property `path`)
9
+ * and states the fix in `message`, because the author reading it is mid-body
10
+ * and needs the correction, not a classification.
11
+ *
12
+ * The codes are a closed schema literal so a caller can switch on them, and so
13
+ * a new refusal is a deliberate addition rather than a new free-form string.
14
+ *
15
+ * @since 0.1.0
16
+ */
17
+
18
+ import * as Schema from "effect/Schema"
19
+
20
+ /**
21
+ * The stable code of a build refusal.
22
+ *
23
+ * `planned_value_computed` is a body computing on a step result;
24
+ * `invalid_all_member` is a non-node handed to {@link module:Node.all};
25
+ * `invalid_join` is a race, any, or quorum with a non-node member, no members,
26
+ * or a quorum count its members cannot reach;
27
+ * `invalid_continuation` is a branch arm or continuation that did not return a
28
+ * node; `recursion_requires_boundary` is a flow calling itself inline, which
29
+ * has to become a trampoline handoff or an explicit child boundary;
30
+ * `placement_requires_boundary` is an inline call whose callee declares a
31
+ * placement the enclosing flow cannot satisfy, which has to become that same
32
+ * explicit child boundary; `cyclic_payload` is a payload that contains itself,
33
+ * which no plan could serialize or hash; `payload_too_deep` is a payload
34
+ * nested past the build bound, which has to be flattened into shallower data;
35
+ * `graph_too_deep` is authored topology nested past the build bound, which
36
+ * has to be split with `.child()` boundaries or trampoline handoffs;
37
+ * `duplicate_node` is two structural graph addresses resolving to one durable
38
+ * node id;
39
+ * `invalid_priority` is a scheduling priority that is not a safe integer,
40
+ * which no ordering could compare; `invalid_payload` is a payload member that
41
+ * cannot be captured as inert JSON without executing author code or losing
42
+ * identity; `unstable_callback` is a function with process-local identity in a
43
+ * graph that requires stable callbacks. Declare its complete inert captures
44
+ * with `Node.capture`, including semantic implementation versions. The two
45
+ * depth refusals exist because graph building walks with an explicit stack
46
+ * and refuses at a bound, rather than recursing until the native stack
47
+ * overflows without a typed error.
48
+ *
49
+ * The four effect-authority refusals are raised when a declaration claims more
50
+ * than the declaration enclosing it granted:
51
+ * `effect_outside_envelope` is a read or write path the envelope does not
52
+ * cover, and it names those paths in `path`; `effect_mode_widening` is an
53
+ * `expected` declaration inside a `hermetic` envelope; `effect_tier_widening`
54
+ * is a tier less reversible than the envelope's; and
55
+ * `capability_outside_grant` is a called flow or action requiring a capability
56
+ * the caller does not hold, which names the dropped capabilities in `path`. The
57
+ * first three are fatal by {@link isFatalDiagnostic}; the fourth is advisory,
58
+ * because dropping a capability narrows authority rather than widening it.
59
+ *
60
+ * The last six are raised by `@smthrs/core`'s graph builder, which uses this
61
+ * one error rather than a second class of its own. `write_conflict` is two work
62
+ * nodes overlapping under `onConflict: "fail"`, and `node` names the first of
63
+ * the pair while `Graph.conflicts` carries both; `missing_key_material` is a
64
+ * node that reached key compilation without any; `dependency_cycle` is a
65
+ * dependency set that cannot be ordered, and `node` names a node in the cycle;
66
+ * `plan_too_large` is a node, edge, conflict, or effect-path limit crossed, and
67
+ * `node` names the node whose admission crossed it; `payload_too_large` is a
68
+ * plan value with more members than the bound, and `path` names the offending
69
+ * value path; `invalid_node` is a malformed node AST. All six are fatal.
70
+ *
71
+ * @since 0.1.0
72
+ * @category schemas
73
+ * @slop
74
+ */
75
+ export const GraphBuildErrorCode = Schema.Literals([
76
+ "planned_value_computed",
77
+ "invalid_all_member",
78
+ "invalid_join",
79
+ "invalid_continuation",
80
+ "recursion_requires_boundary",
81
+ "placement_requires_boundary",
82
+ "cyclic_payload",
83
+ "payload_too_deep",
84
+ "graph_too_deep",
85
+ "duplicate_node",
86
+ "invalid_priority",
87
+ "invalid_payload",
88
+ "unstable_callback",
89
+ "effect_outside_envelope",
90
+ "effect_mode_widening",
91
+ "effect_tier_widening",
92
+ "capability_outside_grant",
93
+ "write_conflict",
94
+ "missing_key_material",
95
+ "dependency_cycle",
96
+ "plan_too_large",
97
+ "payload_too_large",
98
+ "invalid_node"
99
+ ])
100
+
101
+ /**
102
+ * The value form of {@link GraphBuildErrorCode}.
103
+ *
104
+ * @since 0.1.0
105
+ * @category models
106
+ * @slop
107
+ */
108
+ export type GraphBuildErrorCode = typeof GraphBuildErrorCode.Type
109
+
110
+ /**
111
+ * A build the planner refuses, naming the site and the fix.
112
+ *
113
+ * `node` is the node reference the failure belongs to — a planned value's
114
+ * origin node, an `all` member name, a branch arm, or the node whose admission
115
+ * crossed a bound. `path` is the property path recorded on a planned value
116
+ * before it was misused, the effect paths an over-claiming declaration named,
117
+ * the capabilities a call lost, or the value path of an oversized payload; it
118
+ * is empty for every other code.
119
+ *
120
+ * This is the ONE build refusal. `@smthrs/core`'s graph builder raised a second
121
+ * `GraphBuildError` of its own, with `paths`, `nodeId` and `nodes` in place of
122
+ * these fields, and the two code sets diverged instead of merging. A conflict's
123
+ * second node is read from `Graph.conflicts`, which carries the pair.
124
+ *
125
+ * @since 0.1.0
126
+ * @category errors
127
+ * @slop
128
+ */
129
+ export class GraphBuildError extends Schema.TaggedError<GraphBuildError>()("@smthrs/plan/GraphBuildError", {
130
+ code: GraphBuildErrorCode,
131
+ node: Schema.String,
132
+ path: Schema.Array(Schema.String),
133
+ message: Schema.String
134
+ }) {}
135
+
136
+ /**
137
+ * The one advisory code. Everything else a build records blocks the drafts.
138
+ *
139
+ * A capability the caller does not grant is DROPPED, not granted: the callee
140
+ * runs with less authority than it asked for, which is the safe direction. The
141
+ * author still needs to know, because a flow that silently loses a capability
142
+ * fails later at the action that needed it, so the build records the refusal
143
+ * and lets the graph compile.
144
+ */
145
+ const advisoryCodes: ReadonlySet<GraphBuildErrorCode> = new Set<GraphBuildErrorCode>(["capability_outside_grant"])
146
+
147
+ /**
148
+ * Whether a recorded refusal blocks the drafts a plan is compiled from.
149
+ *
150
+ * A fatal refusal means the graph describes something no plan could execute as
151
+ * written, so the drafts are withheld. An advisory one reports a narrowing the
152
+ * author should see while the graph still compiles.
153
+ *
154
+ * @since 0.1.0
155
+ * @category predicates
156
+ * @slop
157
+ */
158
+ export const isFatalDiagnostic = (diagnostic: GraphBuildError): boolean => !advisoryCodes.has(diagnostic.code)
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The key-material contract: what a planner hands the step-key compiler.
3
+ *
4
+ * Revived from the deleted `packages/smithers/flows/keys/src/KeyMaterial.ts`.
5
+ * It does NOT return to `@smthrs/keys`: that package owns generic derivation
6
+ * and stored-key validation, while material is a *plan-shaped* concept. It
7
+ * lives here, above `@smthrs/keys`, so the key package stays a leaf.
8
+ *
9
+ * Governing contract: the step-key rules at
10
+ * https://smithers.sh/docs/concepts/content-addressing. Material is computed with no I/O,
11
+ * and two hardening rules shape it: the nominal, never shape-sniffed
12
+ * `InputRef` and the hashed `version`.
13
+ *
14
+ * @since 0.1.0
15
+ */
16
+
17
+ import * as Schema from "effect/Schema"
18
+ import * as Effects from "./Effects.ts"
19
+
20
+ /**
21
+ * A reference to one declared input of a node: a value hashed inline, or the
22
+ * key of an upstream node whose result this node consumes.
23
+ *
24
+ * The three variants are tagged, and the tag is hashed, so `Pending{from}` and
25
+ * `Ref{from, path: []}` — which resolve to the same dependency digest — can
26
+ * never collide.
27
+ *
28
+ * @since 0.1.0
29
+ * @category schemas
30
+ * @slop
31
+ */
32
+ export const InputRef = Schema.Union([
33
+ Schema.TaggedStruct("Literal", { value: Schema.Unknown }),
34
+ Schema.TaggedStruct("Ref", { from: Schema.NonEmptyString, path: Schema.Array(Schema.String) }),
35
+ Schema.TaggedStruct("Pending", { from: Schema.NonEmptyString })
36
+ ])
37
+
38
+ /**
39
+ * A reference to one declared input of a node.
40
+ *
41
+ * @since 0.1.0
42
+ * @category models
43
+ * @slop
44
+ */
45
+ export type InputRef = typeof InputRef.Type
46
+
47
+ /**
48
+ * The material version. Folded into every hashed body so a bump re-keys every
49
+ * node derived from it — the original's second collision fix, kept.
50
+ *
51
+ * @since 0.1.0
52
+ * @category constants
53
+ * @slop
54
+ */
55
+ export const version = "flows/key-material/v2"
56
+
57
+ /**
58
+ * Everything that can change a node's result, handed to the digest.
59
+ *
60
+ * `effects` and `placement` are opaque: canonically serialized, never
61
+ * interpreted, which is what keeps the compiler independent of whatever the
62
+ * flow builder decides an effect declaration looks like.
63
+ *
64
+ * @since 0.1.0
65
+ * @category schemas
66
+ * @slop
67
+ */
68
+ export const KeyMaterial = Schema.Struct({
69
+ version: Schema.Literal(version),
70
+ kind: Effects.Tier,
71
+ // Absence claims determinism; only the explicit declaration changes identity.
72
+ nondeterministic: Schema.optional(Schema.Literal(true)),
73
+ body: Schema.Unknown,
74
+ inputs: Schema.Array(InputRef),
75
+ layers: Schema.Array(Schema.String),
76
+ capabilities: Schema.Array(Schema.String),
77
+ effects: Schema.optional(Schema.Unknown),
78
+ placement: Schema.optional(Schema.Unknown)
79
+ })
80
+
81
+ /**
82
+ * Everything that can change a node's result, handed to the digest.
83
+ *
84
+ * @since 0.1.0
85
+ * @category models
86
+ * @slop
87
+ */
88
+ export type KeyMaterial = typeof KeyMaterial.Type
89
+
90
+ /**
91
+ * The graph-local dependencies a material names, in declaration order and
92
+ * without duplicates. The compiler resolves exactly these before hashing, and
93
+ * {@link module:Plan} uses them as the edge set — one derivation, so an edge
94
+ * and a hashed reference can never disagree.
95
+ *
96
+ * @since 0.1.0
97
+ * @category accessors
98
+ * @slop
99
+ */
100
+ export const dependencies = (material: KeyMaterial): ReadonlyArray<string> => {
101
+ const seen = new Set<string>()
102
+ for (const input of material.inputs) {
103
+ if (input._tag !== "Literal") seen.add(input.from)
104
+ }
105
+ return [...seen]
106
+ }