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

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,203 @@
1
+ ---
2
+ title: "The authoring AST"
3
+ description: "Node and Planned: how a flow body describes topology as pure data, why map transforms and branch decides, and how a closure survives as a digest."
4
+ sidebar:
5
+ order: 4
6
+ ---
7
+
8
+ `Node` is the shape a flow body describes, and nothing more. Building a node
9
+ records an inspectable, closure-free, JSON-serializable description and executes
10
+ nothing. `Planned` is the strict placeholder that body sees wherever a step
11
+ result will be.
12
+
13
+ Together they are the layer above the persisted plan. Neither runs anything.
14
+
15
+ ## Building a node runs nothing
16
+
17
+ ```ts
18
+ import * as Node from "@smthrs/plan/Node"
19
+
20
+ const greeting = Node.map(Node.succeed({ name: "world" }), (value) => value.name.toUpperCase())
21
+ ```
22
+
23
+ `greeting` is a value holding an AST. The mapper has not run, and `succeed` has
24
+ produced no string. The same split as a build system's action declaration: you
25
+ describe the work, and something else decides when to do it.
26
+
27
+ The AST has nine variants: `Succeed`, `Fail`, `All`, `Map`, `AndThen`,
28
+ `Branch`, `Catch`, `FlowCall`, and `ActionCall`. `Node.Ast` is the union, and
29
+ every node exposes it as `node.ast`.
30
+
31
+ ## Requirements are phantom
32
+
33
+ A node carries Effect's requirement channel, `R`, and carries it as a phantom.
34
+ Nothing at plan time reads it: the AST, the graph built from it, its key
35
+ material, and every digest are identical whatever `R` says.
36
+
37
+ It exists so a value that names an implementation it does not carry, an action
38
+ call, can say so in its type, and so the place that finally has to run that
39
+ implementation can demand it. Building a plan stays requirement-free; only
40
+ executing one is not.
41
+
42
+ ## Map transforms; branch decides
43
+
44
+ `Node.map` is transformation only. The function is digested, not run: it
45
+ executes later, on the real value. A `map` that decides what happens next is a
46
+ `branch` written wrongly.
47
+
48
+ `Node.branch` takes both arms and evaluates each once, symbolically, against a
49
+ `Planned` placeholder. The resulting ASTs are what the node stores, so a plan
50
+ shows the exit condition and both continuations before it runs. The predicate is
51
+ digested and evaluated at run time on the real value.
52
+
53
+ ```ts
54
+ import { Action } from "@smthrs/flow"
55
+ import * as Node from "@smthrs/plan/Node"
56
+ import type * as Planned from "@smthrs/plan/Planned"
57
+ import * as Schema from "effect/Schema"
58
+
59
+ const Draft = Action.make("docs/Draft", { payload: { topic: Schema.String }, success: Schema.String })
60
+ const Publish = Action.make("docs/Publish", {
61
+ payload: { text: Schema.String, urgent: Schema.Boolean },
62
+ success: Schema.String
63
+ })
64
+
65
+ const article = Node.bindPlanned(
66
+ Draft.call({ topic: "durable plans" }),
67
+ (text: Planned.Planned<string>) =>
68
+ Node.branch(Node.succeed({ urgent: true }), {
69
+ if: (flags) => flags.urgent,
70
+ then: () => Publish.call({ text, urgent: true }),
71
+ else: () => Publish.call({ text, urgent: false })
72
+ })
73
+ )
74
+ ```
75
+
76
+ Both arms contribute their requirements, because both arms are topology the plan
77
+ carries. A run takes one of them, but which one is not known until the predicate
78
+ sees the real value, so an execution has to be able to take either.
79
+
80
+ `Node.catch` is the same idea for failures: the protected graph and the failure
81
+ arm are both stored, and the arm is built once at plan time against a strict
82
+ planned error placeholder. With no schema the whole typed error channel is
83
+ handled; with one, only the values it accepts, and the remainder survives in the
84
+ resulting error type.
85
+
86
+ ## There is no loop node
87
+
88
+ A plan is always a DAG, so this module has no loop and never will. Repetition
89
+ lives one level up, in what a flow settles with.
90
+
91
+ `Node.all`, `Node.race`, `Node.any`, and `Node.quorum` have the same boundary. Its width is fixed at plan time. Fanning out
92
+ over something a step discovered is not `all`: end the round and carry the list
93
+ in the next flow's payload, where it is real data.
94
+
95
+ ## Planned values
96
+
97
+ A `Planned<T>` may be passed into a payload field, into a branch, or into a map,
98
+ and field access is allowed, because it records a reference path. It may never
99
+ be computed on.
100
+
101
+ Misuse fails twice. The type is branded, so arithmetic on a planned value is a
102
+ compile error. The proxy's `Symbol.toPrimitive`, `valueOf`, `toString`,
103
+ `toJSON`, application, `in`, and enumeration traps throw a `GraphBuildError`
104
+ rather than let a plan be built around `NaN` or `"[object Object]"`, which
105
+ catches template interpolation, `String(value)`, and `JSON.stringify` of a
106
+ payload holding one.
107
+
108
+ ```ts
109
+ import * as Planned from "@smthrs/plan/Planned"
110
+
111
+ declare const files: Planned.Planned<{ readonly count: number }>
112
+
113
+ /** Records the path ["count"] and stays a placeholder. */
114
+ const count: Planned.Planned<number> = files.count
115
+ ```
116
+
117
+ Reading `files.count` is a reference. Interpolating it into a string is
118
+ computation, and the refusal names both the node and the recorded path, so the
119
+ message says which value in which body was misused.
120
+
121
+ JavaScript exposes no trap for `Boolean(value)` or strict identity, so those
122
+ cannot be refused at run time. They reveal only proxy truthiness or identity and
123
+ never the planned result. Use `Node.branch` for a decision on a real value.
124
+
125
+ `Planned.TypeId` is interned, so a value that crossed a module boundary is still
126
+ recognized. Interning is a recognition aid rather than a capability:
127
+ `Planned.reference` returns a reference only when the value stored under that
128
+ symbol has the complete `{node, path}` shape.
129
+
130
+ ## Payloads become inert JSON
131
+
132
+ A payload is stored as its inert JSON mirror. A data-valued callable `toJSON` is
133
+ honored, so a `Date` or a `URL` keys as the value it serializes to rather than
134
+ as an empty object. A function or symbol member is dropped from an object and
135
+ becomes `null` in an array. Shared references and cycles clone as they were
136
+ written.
137
+
138
+ Accessors and unsupported prototypes without `toJSON` fail as `invalid_payload`.
139
+ A `toJSON` that returns its own receiver fails as `cyclic_payload` rather than
140
+ collapsing to an empty object. The clone and the input therefore key identically
141
+ or refuse together.
142
+
143
+ ## Functions survive as digests
144
+
145
+ The functions an author writes, a mapper, a continuation, a branch predicate,
146
+ live in `WeakMap`s keyed by the AST node they belong to. The AST keeps only a
147
+ `FunctionIdentity` digest of the function's exact source.
148
+
149
+ Exact source matters, because whitespace inside a string literal is behavior.
150
+
151
+ A function whose inert captures were declared with `Node.capture` digests those
152
+ captures and gets deterministic identity. Every other function additionally
153
+ carries process-local, per-function entropy, so two indistinguishable closure
154
+ sources fail closed instead of sharing a cache key.
155
+
156
+ ```ts
157
+ import * as Node from "@smthrs/plan/Node"
158
+
159
+ const suffixed = Node.capture({ suffix: "!" }, (value: string) => `${value}!`)
160
+ ```
161
+
162
+ `capture` refuses a capture record nested past 256 levels with a path-bearing
163
+ error rather than overflowing the native stack, and refuses accessors, exotic
164
+ prototypes, symbols, and cycles for the same reason it refuses them in a
165
+ payload: an identity that cannot describe the function's behavior is worse than
166
+ no identity.
167
+
168
+ ## Recognizing a node
169
+
170
+ `Node.isNode` recognizes a node this package built, by registration at
171
+ construction. It also recognizes a rehydrated node, an object sharing the node
172
+ prototype whose own `ast` is a well-formed AST, by that shape, because
173
+ [`@smthrs/flow`](/api/flow) hands an AST that crossed a serialization boundary
174
+ back as a node.
175
+
176
+ `Node.TypeId` is a public string any object can carry and counts for nothing on
177
+ its own. Every combinator that admits a node reads its `ast` as trusted
178
+ topology, so an object carrying the marker on any other prototype, one
179
+ inheriting it from a node, and one whose `ast` is missing, malformed, or cyclic
180
+ are all refused with the same `GraphBuildError` as any other non-node. A proxy
181
+ is judged by the shape it forwards.
182
+
183
+ ## How a flow call joins the plan
184
+
185
+ `Node.flowCall` takes a mode. `inline` splices the callee's body into the
186
+ caller's plan, `boundary` makes the call one child execution, and `handoff`
187
+ names the next trampoline round.
188
+
189
+ The child call is `FlowCall{mode: "boundary"}` rather than a second AST tag
190
+ beside `FlowCall`. The three modes are one authoring construct, a call to a
191
+ named flow with a payload, differing only in how the plan joins it, and every
192
+ consumer (`Graph.build`'s expansion test, key material, the interpreter's
193
+ dispatch) already switches on `mode`. A parallel `ChildCall` tag would duplicate
194
+ the flow tag, payload, and declaration side table in every one of them.
195
+
196
+ ## The engine members
197
+
198
+ `Node.flowCall`, `actionCall`, `declaration`, `continuation`, `mapper`,
199
+ `predicate`, `catchFilter`, and `functionIdentity` exist for
200
+ [`@smthrs/flow`](/api/flow), which owns flow and action authoring. They are
201
+ supported names, not authoring API: they validate nothing, and several of them
202
+ answer `undefined` for an AST that was rehydrated from JSON, because the side
203
+ tables holding the real functions do not survive serialization.
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: "Declared effects and conflicts"
3
+ description: "How declared reads and writes become ordering: the overlap pass, the reader-after-writer pass, the three verdicts, and the cycles both passes refuse."
4
+ sidebar:
5
+ order: 3
6
+ ---
7
+
8
+ Every node declares what it does to the workspace, as paths. Two plan-time
9
+ passes read those declarations and turn them into edges, so a scheduler never
10
+ has to guess whether two pieces of work can run at once.
11
+
12
+ ```ts
13
+ import * as Plan from "@smthrs/plan/Plan"
14
+
15
+ const effects: Plan.NodeEffects = {
16
+ reads: [{ _tag: "Glob", include: ["src/**/*.ts"] }],
17
+ writes: ["dist/bundle.js"],
18
+ removes: ["dist/stale.js"],
19
+ boundaryMode: "hard"
20
+ }
21
+ ```
22
+
23
+ Bare string entries are literal paths at both overlap analysis and execution,
24
+ even when they contain `*` or `**`. Wildcards belong in `Glob.include` and
25
+ `Glob.exclude`.
26
+
27
+ Measuring a path is run-time work, so a digest here would break the no-I/O law
28
+ that makes a plan reproducible.
29
+
30
+ ## What a node produces
31
+
32
+ `removes` is optional, and a removal mutates the world exactly as a write does.
33
+ Both passes therefore fold `writes` and `removes` into one produced set. A path
34
+ declared as both is refused as `invalid_effects`, because the declaration
35
+ contradicts itself.
36
+
37
+ `boundaryMode` is `hard` or `expected`. It travels with the declaration into the
38
+ measured boundary at execution time; the plan passes do not interpret it.
39
+
40
+ ## The overlap pass
41
+
42
+ Declared write sets make overlap detectable before anything runs. The pass
43
+ compares every pair of producing nodes and skips a pair the graph already
44
+ orders, including one ordered through an ordering edge this same pass just
45
+ inferred. Every other overlapping pair is annotated on both members with the
46
+ resolved verdict.
47
+
48
+ | Verdict | Effect |
49
+ | ----------- | --------------------------------------------------------------------------------- |
50
+ | `serialize` | The default. The later writer gains an ordering edge. |
51
+ | `lane` | Both writers get lane annotations when either asks for one, and no ordering edge. |
52
+ | `fail` | `compile` fails with `overlap_forbidden`, for flows that promise disjointness. |
53
+
54
+ `fail` dominates `lane`, which dominates `serialize`. A flow that promised
55
+ disjointness must not be quietly serialized, and a pair where either side asked
56
+ for a lane must not be silently ordered.
57
+
58
+ Each annotation also carries the runtime strategy the pair resolved to, where
59
+ `stop-merge` dominates `delay-rebase`. That is what the scheduler does when the
60
+ predicted overlap actually bites.
61
+
62
+ A node states its own preferences on its draft, through `conflictStrategy` and
63
+ `runtimeStrategy`. The annotation is a property of the _pair_, not of one
64
+ declaration, which is why both preferences are recorded on the node and resolved
65
+ per pair.
66
+
67
+ Nodes frozen by an earlier generation are annotated on the new node only,
68
+ because their rows are append-only.
69
+
70
+ ## The reader-after-writer pass
71
+
72
+ The overlap pass compares write sets against write sets, so a node that _reads_
73
+ a path another node _writes_ is ordered by nothing. Reader and writer could be
74
+ admitted in the same wavefront round; the reader would then measure pre-producer
75
+ bytes and, because the dispatch key honestly folds the digest it measured, cache
76
+ that wrong execution as a legitimate one.
77
+
78
+ The second pass closes that. A reader whose read set overlaps a producer's
79
+ produced set gains an ordering edge to the producer, unless the graph already
80
+ orders it there or an explicit `Ref`/`Pending` dependency path selects a read
81
+ before that writer.
82
+
83
+ Like a `serialize` edge, the edge enters `dependsOn` and never key material. The
84
+ reader computes the same result either way, and its content dependence is
85
+ already keyed by the hermetic boundary digests measured at dispatch.
86
+
87
+ ## When an edge would close a cycle
88
+
89
+ An explicit `Ref`/`Pending` dependency path that orders the writer after the
90
+ reader is accepted, including a transitive path. It selects earlier bytes: the
91
+ initial source or an earlier producer's output. The pass preserves that order
92
+ instead of adding a reverse edge.
93
+
94
+ If only inferred producer or `serialize` edges order the writer after the
95
+ reader, there is no explicit selection of earlier bytes. Adding the required
96
+ reader-after-writer edge closes a cycle, so the plan is refused with `cycle`.
97
+ The message names the reader, the overlapping paths, the producer, and the
98
+ contradictory dependency chain. For example, if the chain contains inferred
99
+ edges and no declared-only path selects the earlier read:
100
+
101
+ ```text
102
+ Plan cycle: node lint reads dist/bundle.js, which node bundle produces, so lint must
103
+ follow bundle, but bundle already depends on lint through bundle -> report -> lint
104
+ ```
105
+
106
+ Reachability through declared `Ref`/`Pending` edges establishes explicit
107
+ sequencing. Declaration order alone does not select earlier bytes.
108
+
109
+ ## Overlap is conservative
110
+
111
+ `FileSet.overlaps` answers `true` whenever two declarations _might_ share a
112
+ path. Over-serializing costs latency; under-serializing costs correctness, so
113
+ the comparison is deliberately asymmetric: `false` proves that no path can
114
+ belong to both declarations.
115
+
116
+ Two globs always overlap, and so do a glob and a tree artifact, because
117
+ comparing pattern languages exactly is not worth the risk of being wrong.
118
+
119
+ Exact paths compare in canonical form: every separator rewritten to `/` and
120
+ Unicode normalized to NFC. Without that, `dist\bundle.js` and `dist/bundle.js`
121
+ would be two spellings of one file that the overlap check could not see, which
122
+ is the same class of hole that `.` segments and empty segments would open.
123
+ [Declare the files a node touches](../guides/declare-file-effects.md) covers the
124
+ vocabulary and the forms `workspaceRelative` refuses.
125
+
126
+ ## What this buys the scheduler
127
+
128
+ A plan arrives at the scheduler with every predictable conflict already
129
+ resolved into an edge or an annotation. Two nodes with no path between them and
130
+ no annotation between them are safe to run at once, and that fact was
131
+ established before a single byte was read.
132
+
133
+ The annotations that remain describe the cases a plan cannot resolve alone: two
134
+ lane writers that a runtime has to keep apart, and the runtime strategy each
135
+ pair agreed on if the predicted overlap turns real.
@@ -0,0 +1,168 @@
1
+ ---
2
+ title: "The plan value"
3
+ description: "What a compiled plan is: a keyed graph, two digests, a generation counter, and a snapshot nothing can mutate after the fact."
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ A plan is the whole of what a run intends to do, computed before anything runs
9
+ and expressed as one value. It is a `Plan` struct: an id, the flow it was
10
+ compiled for, a generation counter, two digests, and an ordered list of keyed
11
+ nodes.
12
+
13
+ ```ts
14
+ import * as Plan from "@smthrs/plan/Plan"
15
+
16
+ declare const plan: Plan.Plan
17
+ plan.planId // "review-4821"
18
+ plan.flow // "example/Review"
19
+ plan.generation // 0
20
+ plan.baseDigest // the digest a human approved
21
+ plan.digest // the digest as of this generation
22
+ plan.nodes // every keyed node, in material-dependency order
23
+ ```
24
+
25
+ Everything else in this package either produces that value, persists it,
26
+ compares two of them, or supplies the vocabulary its fields are written in.
27
+
28
+ ## The four rules
29
+
30
+ Four properties shape every design decision here. Read them once and most of the
31
+ API stops being surprising.
32
+
33
+ ### Planning demands nothing
34
+
35
+ A `Node` carries Effect's requirement channel, `R`, as a phantom. No combinator
36
+ reads it, so building a graph asks for no service at all. The channel states
37
+ which implementations running the plan will need, and the place that runs it is
38
+ where the compiler asks for them.
39
+
40
+ `Plan.compile` is the one exception, and a narrow one: it asks for Effect's
41
+ `Crypto` service, because it hashes. It asks for nothing else.
42
+
43
+ ### Planning performs no I/O
44
+
45
+ Nothing here reads a file, a clock, or a network. A node's declared `effects`
46
+ carry read and write _paths_, never digests, because measuring a path is the
47
+ scheduler's run-time job. That is what makes a plan reproducible: compile the
48
+ same drafts twice, on two machines, and you get the same keys and the same
49
+ digest.
50
+
51
+ ### Invalidation is re-keying
52
+
53
+ A node's key is a function of what it consumes, so an edited declaration
54
+ re-keys that node and its dependent cone and nothing else. There is deliberately
55
+ no reverse-dependency index and no invalidating node visitor in this package,
56
+ and there must never be one: content addressing subsumes both. [Step keys](./step-keys.md) covers what a key folds in.
57
+
58
+ ### A plan grows; it is never rewritten
59
+
60
+ `Plan.append` adds a pre-keyed subgraph at the next generation. Recorded nodes
61
+ keep their id, key, edges, and generation byte for byte, and the SQL raises
62
+ rather than letting a caller update or delete one. Re-ordering after a
63
+ reconciliation happens by re-keying _future_ steps, never by rewriting history.
64
+
65
+ ## Nodes
66
+
67
+ Within each generation, `plan.nodes` follows the topological order of material dependencies.
68
+ Inferred reader-after-writer edges can point to later array entries. Schedule
69
+ nodes from the complete `dependsOn` graph.
70
+
71
+ Each entry of `plan.nodes` is a `PlanNode`:
72
+
73
+ | Field | What it holds |
74
+ | ------------ | ------------------------------------------------------------------------------ |
75
+ | `id` | The durable lookup address. Never hashed, so renaming a node re-keys nothing. |
76
+ | `kind` | `step`, `agent`, or `merge`. |
77
+ | `key` | The computed step key, in the same `key1_` format the engine dispatches under. |
78
+ | `material` | The declaration the key was derived from. |
79
+ | `effects` | The reads, writes, removals, and boundary mode this node declares. |
80
+ | `dependsOn` | The scheduling edge set, including dependencies on later array entries. |
81
+ | `conflicts` | One annotation per overlapping writer no dependency path already orders. |
82
+ | `strategy` | This declaration's preferred plan-time verdict for an overlap. |
83
+ | `runtime` | This declaration's preferred response when a predicted overlap actually bites. |
84
+ | `priority` | The scheduler's tie-break among ready work. Higher runs first. |
85
+ | `generation` | Which append added this node. Generation 0 is what `compile` produced. |
86
+
87
+ `dependsOn` is wider than the material references alone. It also carries the
88
+ ordering edge a `serialize` verdict added and the reader-after-writer edges that
89
+ put a node behind whoever produces the paths it reads. Ordering edges are
90
+ deliberately not key material: a node serialized behind another still computes
91
+ the same result, so re-keying it would throw away a legitimate cache hit.
92
+ [Declared effects and conflicts](./effects-and-conflicts.md) covers both passes.
93
+
94
+ `priority` is not key material either, for the same reason. It changes latency
95
+ and nothing else.
96
+
97
+ ## Two digests
98
+
99
+ `baseDigest` is the digest at generation 0: what a human approved and what a
100
+ running run pins. `digest` advances with every appended elaboration. A control
101
+ plane binds an approval to `baseDigest`, so a plan that grew during the run
102
+ still validates against the decision that admitted it.
103
+
104
+ Both digests cover node identity, every computed key, the edge set, the conflict
105
+ annotations, the declared effects, the priority, and each node's own conflict
106
+ and runtime strategies. They do not cover anything presentational, and they do
107
+ not cover a diff's attribution report.
108
+
109
+ The command line prints the same projection: [`smthrs flow plan`](/cli/flow)
110
+ renders the plan card and the approval payload that
111
+ [`smthrs approvals approve`](/cli/approvals) and `smthrs flow execute` accept.
112
+
113
+ ## Generations
114
+
115
+ `compile` produces generation 0. Every `append` produces the next one, and
116
+ `Plan.generationNodes` reads back only the nodes the newest generation added,
117
+ which is exactly what [`@smthrs/plan-store`](/api/plan-store)'s `PlanStore.append` inserts.
118
+
119
+ A generation exists because plans elaborate. A step that discovers the shape of
120
+ its own follow-on work cannot state that work up front, so the plan grows to
121
+ hold it. What it must never do is change what an operator already saw, which is
122
+ why growth is the only move available.
123
+
124
+ ## An immutable snapshot
125
+
126
+ A compiled plan is a deep-frozen snapshot of the drafts it was given. Material
127
+ is stored as the inert JSON mirror its key already covers, so a `Date`, a `URL`,
128
+ or any value with a data-valued callable `toJSON` is stored as the value it
129
+ serializes to. Mutating a caller's draft after compiling cannot change the plan,
130
+ its keys, or its digest.
131
+
132
+ A material accessor, or a prototype with no JSON representation, is refused as
133
+ `invalid_node` naming the node and the payload path rather than stored by
134
+ reference. A `Planned` placeholder is left intact so canonical serialization
135
+ still refuses it.
136
+
137
+ ## Payloads are plaintext
138
+
139
+ Material is identity: the body and inputs of a node are hashed into its step
140
+ key and written verbatim as plaintext into `node_json` and the approval card an
141
+ operator reads. This package never redacts them, and redacting after hashing
142
+ would break `Plan.verify`. Keep credentials out of payloads. A secret belongs in
143
+ a layer, a capability, or the environment, resolved at dispatch.
144
+
145
+ ## Where a plan comes from
146
+
147
+ `Plan.compile` takes `NodeDraft` values. Flow authors do not write those by
148
+ hand: [`@smthrs/flow`](/api/flow) walks a flow body into drafts through
149
+ `Graph.build` and `Graph.drafts`, and hands them here. `Plan.NodeDraft` is the
150
+ type in the middle, and it is the whole contract between the two packages.
151
+
152
+ [Author a node graph](../guides/author-a-node-graph.md) walks that seam.
153
+
154
+ ## Bounds
155
+
156
+ One compiled plan holds at most `Plan.maximumPlanNodes` nodes, which is 10,000.
157
+ A plan above the ceiling is refused with `graph_too_large` before effect
158
+ analysis. Analysis also refuses more than 250,000 candidate pairs or 10,000,000
159
+ work units, including overlap comparisons, bitset merges and graph traversal.
160
+ This budget applies across all generations replayed by `verify`. Split plans
161
+ that exceed either budget across flow boundaries.
162
+
163
+ Reachability is cached in bitsets and updated when ordering edges are inferred.
164
+ Analysis yields periodically so cancellation and other fibers can run. Imported
165
+ plans reuse one effect expansion map and candidate index across generations.
166
+
167
+ Compilation itself walks with explicit stacks and never recurses per edge, so
168
+ depth is a data structure rather than native stack frames.