@smthrs/plan 1.0.0-rc.6 → 1.0.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ This package declares `effect` as a `^4.0.4` peer dependency. Install one
6
6
  `effect` release in that range so all Smithers packages share one Effect
7
7
  runtime.
8
8
 
9
- **Documentation:** https://plan.smithers.sh
9
+ **Documentation:** https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan/docs
10
10
 
11
11
  The persisted plan: a keyed action graph, its append-only store, its diff, and
12
12
  the step-key compiler that gives every node its identity.
@@ -17,7 +17,7 @@ from what that node declares, records that graph in a SQLite store that only
17
17
  ever grows, and compares two graphs to say exactly what changed. It runs
18
18
  nothing itself: it performs no I/O beyond the database, and driving a compiled
19
19
  plan is a scheduler's job, such as
20
- [`@smthrs/engine-store`](https://engine-store.smithers.sh)'s `PlanScheduler`.
20
+ [`@smthrs/engine-store`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/engine-store/docs)'s `PlanScheduler`.
21
21
 
22
22
  Reach for it when you are building a scheduler, a cache, or an approval flow
23
23
  over declared work and you need step identity, declared file effects, and an
@@ -35,8 +35,8 @@ pnpm add @effect/platform-node @effect/platform-node-shared
35
35
  ```
36
36
 
37
37
  Node.js 26.4.0 or later, and `effect` `^4.0.4` as a peer. Recording a plan
38
- is [`@smthrs/plan-store`](https://plan-store.smithers.sh), which owns the SQL
39
- and brings [`@smthrs/database`](https://database.smithers.sh) with it.
38
+ is [`@smthrs/plan-store`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan-store/docs), which owns the SQL
39
+ and brings [`@smthrs/database`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/database/docs) with it.
40
40
 
41
41
  ## Compile a plan
42
42
 
@@ -100,8 +100,8 @@ another number and both keys move; rename `read-pr` to `fetch-pr` and neither
100
100
  does.
101
101
 
102
102
  Recording that plan is
103
- [`@smthrs/plan-store`](https://plan-store.smithers.sh), whose
104
- [quickstart](https://plan-store.smithers.sh/quickstart/) builds the whole
103
+ [`@smthrs/plan-store`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan-store/docs), whose
104
+ [quickstart](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan-store/docs/quickstart.md) builds the whole
105
105
  composition against an in-memory database.
106
106
 
107
107
  ## What is in here
@@ -168,7 +168,7 @@ edge: `compile` puts the reader behind its producer by growing `dependsOn`, and
168
168
  fails with `cycle` when no edge set can satisfy both that and the declared
169
169
  order.
170
170
 
171
- [Declared effects and conflicts](https://plan.smithers.sh/concepts/effects-and-conflicts/)
171
+ [Declared effects and conflicts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/docs/concepts/effects-and-conflicts.md)
172
172
  covers both passes in full.
173
173
 
174
174
  ## When it refuses
@@ -183,9 +183,9 @@ through `GraphBuildError`, whose closed code set names the site and the fix;
183
183
  `unstable_callback` in that set is raised by `@smthrs/flow` when a stable build
184
184
  meets a callback without a `Node.capture` declaration. Persistence has its own
185
185
  refusals, all of them
186
- [`@smthrs/plan-store`](https://plan-store.smithers.sh)'s.
186
+ [`@smthrs/plan-store`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan-store/docs)'s.
187
187
 
188
- [Troubleshooting](https://plan.smithers.sh/troubleshooting/) lists every code
188
+ [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/docs/troubleshooting.md) lists every code
189
189
  with the change that clears it.
190
190
 
191
191
  ## Paths, payloads, and immutability
@@ -220,10 +220,10 @@ Persistence is a separate package, which is where a platform choice enters.
220
220
 
221
221
  ## Links
222
222
 
223
- - [Documentation](https://plan.smithers.sh)
224
- - [API reference](https://plan.smithers.sh/reference/api/)
225
- - [`@smthrs/plan-store`](https://plan-store.smithers.sh), the append-only store
223
+ - [Documentation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan/docs)
224
+ - [API reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/docs/api.md)
225
+ - [`@smthrs/plan-store`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan-store/docs), the append-only store
226
226
  a compiled plan is recorded in
227
- - [`@smthrs/flows`](https://flows.smithers.sh), the whole engine as a single
227
+ - [`@smthrs/flows`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/docs), the whole engine as a single
228
228
  dependency, which re-exports this package as its `Plan` namespace
229
229
  - [License: MIT](./LICENSE)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Diff two plans"
3
- description: "Compare a re-plan against the plan before it: what the verdict is, how a re-key is attributed to the field that caused it, and why the report is in no digest."
3
+ description: "Compare a re-plan against the plan before it: what the verdict is, how a re-key is attributed to the field that caused it, why node ids are positional, and why the report is in no digest."
4
4
  sidebar:
5
5
  order: 5
6
6
  ---
@@ -8,28 +8,81 @@ sidebar:
8
8
  "What is different about this run" is a set comparison, not archaeology.
9
9
  `PlanDiff.diff` is that comparison as a value.
10
10
 
11
+ ## Before you start
12
+
13
+ The examples run with `node <file>.ts` on Node.js 26.4.0 or later and need no
14
+ engine, database, or jj. Compiling a plan needs Effect's `Crypto` service, which
15
+ `@effect/platform-node` supplies. In a project that installed `@smthrs/flows`,
16
+ add this package and `@smthrs/flow` at the version of `@smthrs/flows` you have,
17
+ plus the platform package:
18
+
19
+ ```bash
20
+ npm install @smthrs/plan@1.0.0-rc.7 @smthrs/flow@1.0.0-rc.7 @effect/platform-node
21
+ ```
22
+
23
+ The outputs below are real output from 1.0.0-rc.6.
24
+
25
+ ## Compare a re-plan
26
+
27
+ Create `diff.ts`. It compiles the two drafts from
28
+ [Compile drafts into a plan](./compile-a-plan.md), edits one field of one
29
+ draft's body, compiles again, and compares:
30
+
11
31
  ```ts
12
32
  import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
33
+ import * as KeyMaterial from "@smthrs/plan/KeyMaterial"
13
34
  import * as Plan from "@smthrs/plan/Plan"
14
35
  import * as PlanDiff from "@smthrs/plan/PlanDiff"
15
- import type * as Crypto from "effect/Crypto"
16
36
  import * as Effect from "effect/Effect"
17
37
 
18
- /** The drafts and the compiled effect from "Compile drafts into a plan". */
19
- declare const readPr: Plan.NodeDraft
20
- declare const runTests: Plan.NodeDraft
21
- declare const compiled: Effect.Effect<Plan.Plan, never, Crypto.Crypto>
38
+ const readPr: Plan.NodeDraft = {
39
+ id: "read-pr",
40
+ material: {
41
+ version: KeyMaterial.version,
42
+ kind: "sealed",
43
+ body: { action: "read-pr", pr: 4821 },
44
+ inputs: [],
45
+ layers: [],
46
+ capabilities: ["net:get"]
47
+ },
48
+ effects: { reads: [], writes: ["pr.json"], boundaryMode: "hard" }
49
+ }
50
+
51
+ const runTests: Plan.NodeDraft = {
52
+ id: "run-tests",
53
+ material: {
54
+ version: KeyMaterial.version,
55
+ kind: "sealed",
56
+ body: { action: "run-tests" },
57
+ inputs: [{ _tag: "Ref", from: "read-pr", path: [] }],
58
+ layers: [],
59
+ capabilities: []
60
+ },
61
+ effects: { reads: ["pr.json"], writes: ["report.json"], boundaryMode: "hard" }
62
+ }
63
+
64
+ const compile = (nodes: ReadonlyArray<Plan.NodeDraft>) =>
65
+ Plan.compile({ planId: "review-4821", flow: "example/Review", nodes })
22
66
 
23
67
  export const compare = Effect.gen(function*() {
24
- const before = yield* compiled
25
- const after = yield* Plan.compile({
26
- planId: "review-4821",
27
- flow: "example/Review",
28
- nodes: [readPr, { ...runTests, material: { ...runTests.material, body: { action: "run-tests", jobs: 4 } } }]
29
- })
68
+ const before = yield* compile([readPr, runTests])
69
+ const after = yield* compile([
70
+ readPr,
71
+ { ...runTests, material: { ...runTests.material, body: { action: "run-tests", jobs: 4 } } }
72
+ ])
30
73
  const changes: PlanDiff.PlanDiff = PlanDiff.diff(before, after)
31
74
  return changes.rekeyed.map((entry) => `${entry.id}: ${entry.changed.join(", ")}`)
32
75
  }).pipe(Effect.provide(NodeCrypto.layer))
76
+
77
+ console.log(await Effect.runPromise(compare))
78
+ ```
79
+
80
+ ```bash
81
+ node diff.ts
82
+ ```
83
+
84
+ ```text
85
+ [ 'run-tests: body' ]
33
86
  ```
34
87
 
35
88
  `compare` answers `["run-tests: body"]`. The edit moved one field of one node's
@@ -70,6 +123,94 @@ reported as nothing changed.
70
123
  key moved for a reason the comparison does not model. Treat an empty `changed`
71
124
  on a re-keyed node as a bug report, not as noise.
72
125
 
126
+ ## Node ids are positional
127
+
128
+ A node id is the node's structural address in the body, such as
129
+ `root.flow.andThen`, not the name of its step. Appending a step to a flow
130
+ nests the steps before it one level deeper, so a diff by id reports an
131
+ unchanged step as `added` under its new address and reports the addresses it
132
+ left as `rekeyed`. The step's key does not change.
133
+
134
+ Create `positional.ts`. It compiles a two-step flow and the same flow with a
135
+ third step appended, diffs them, then matches the steps by key:
136
+
137
+ ```ts
138
+ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
139
+ import { Action, Flow, Graph } from "@smthrs/flow"
140
+ import { Node } from "@smthrs/plan"
141
+ import * as Plan from "@smthrs/plan/Plan"
142
+ import * as PlanDiff from "@smthrs/plan/PlanDiff"
143
+ import * as Effect from "effect/Effect"
144
+ import * as Schema from "effect/Schema"
145
+
146
+ const Draft = Action.make("fix/Draft", { payload: { issue: Schema.String }, success: Schema.String })
147
+ const Test = Action.make("fix/Test", { payload: { plan: Schema.String }, success: Schema.String })
148
+ const Report = Action.make("fix/Report", { payload: { result: Schema.String }, success: Schema.String })
149
+
150
+ const before = Flow.make("fix/Fix", {
151
+ payload: { issue: Schema.String },
152
+ success: Schema.String,
153
+ body: (input) => Draft.call(input).pipe(Node.bindPlanned((plan) => Test.call({ plan })))
154
+ })
155
+
156
+ /** The same flow with one step appended. Draft and Test are unchanged. */
157
+ const after = Flow.make("fix/Fix", {
158
+ payload: { issue: Schema.String },
159
+ success: Schema.String,
160
+ body: (input) =>
161
+ Draft.call(input).pipe(
162
+ Node.bindPlanned((plan) => Test.call({ plan })),
163
+ Node.bindPlanned((result) => Report.call({ result }))
164
+ )
165
+ })
166
+
167
+ const planOf = (flow: Parameters<typeof Graph.build>[0]) =>
168
+ Plan.compile({ planId: "fix-1", flow: "fix/Fix", nodes: Graph.drafts(Graph.build(flow, { issue: "#42" })) })
169
+
170
+ /** The action a step node runs, read from its key material. */
171
+ const actionOf = (node: Plan.PlanNode) => {
172
+ const body = node.material.body
173
+ return typeof body === "object" && body !== null && "action" in body ? String(body.action) : undefined
174
+ }
175
+
176
+ const program = Effect.gen(function*() {
177
+ const previous = yield* planOf(before)
178
+ const next = yield* planOf(after)
179
+ const changes = PlanDiff.diff(previous, next)
180
+ console.log("added ", changes.added)
181
+ console.log("rekeyed ", changes.rekeyed.map((entry) => entry.id))
182
+ console.log("unchanged", changes.unchanged)
183
+
184
+ // Match steps by key, not by id: an equal key is the same step.
185
+ const recorded = new Set(previous.nodes.map((node) => node.key))
186
+ for (const node of next.nodes) {
187
+ const action = actionOf(node)
188
+ if (action !== undefined) console.log(`${action.padEnd(11)} ${recorded.has(node.key) ? "same key" : "new key"}`)
189
+ }
190
+ }).pipe(Effect.provide(NodeCrypto.layer))
191
+
192
+ await Effect.runPromise(program)
193
+ ```
194
+
195
+ ```bash
196
+ node positional.ts
197
+ ```
198
+
199
+ ```text
200
+ added [ 'root.flow.andThen.andThen', 'root.flow.andThen.then' ]
201
+ rekeyed [ 'root.flow.andThen', 'root.flow.then', 'root.flow', 'root' ]
202
+ unchanged []
203
+ fix/Draft same key
204
+ fix/Test same key
205
+ fix/Report new key
206
+ ```
207
+
208
+ Only `Report` is new. `Draft` and `Test` moved to `root.flow.andThen.andThen`
209
+ and `root.flow.andThen.then` with the keys they had, so they come back under
210
+ `added`; their old ids now hold other nodes and come back under `rekeyed`. To
211
+ ask which steps changed, compare keys rather than ids, as the last loop does: a
212
+ key present in both plans is the same step.
213
+
73
214
  ## Comparison uses the key's own JSON semantics
74
215
 
75
216
  Each compared field is projected through the same JSON mirror the keys are
@@ -88,7 +229,9 @@ The pairing this is built for is a re-plan of the same flow: compile the flow
88
229
  again against the current source and compare the result with the plan an
89
230
  operator last saw. `added` and `removed` are the shape change, `rekeyed` is the
90
231
  work that will not come from cache, and `unchanged` is everything a cache hit
91
- will skip.
232
+ will skip. That reading holds while the shape stays put. Once a step is added
233
+ or removed, ids move, so read the work by key as
234
+ [Node ids are positional](#node-ids-are-positional) shows.
92
235
 
93
236
  The command line prints the same comparison through
94
237
  [`smthrs flow plan`](/cli/flow), and the plan card it renders is what
@@ -96,6 +239,8 @@ The command line prints the same comparison through
96
239
 
97
240
  ## Next
98
241
 
242
+ - [Inspect the plan a body builds](../../../flow/docs/guides/inspect-the-plan.md):
243
+ build a flow's graph and read its nodes before anything runs.
99
244
  - [Step keys](../concepts/step-keys.md): why an upstream edit re-keys a
100
245
  downstream node whose own declaration did not change.
101
246
  - [The plan value](../concepts/plan-value.md): the digests a diff sits beside.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@smthrs/plan",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.6",
4
+ "version": "1.0.0-rc.7",
5
5
  "license": "MIT",
6
6
  "smthrs": {
7
7
  "group": "engine"
@@ -218,9 +218,9 @@
218
218
  "tag": "next"
219
219
  },
220
220
  "dependencies": {
221
- "@smthrs/crypto": "1.0.0-rc.6",
222
- "@smthrs/keys": "1.0.0-rc.6",
223
- "@smthrs/canonical": "1.0.0-rc.6"
221
+ "@smthrs/crypto": "1.0.0-rc.7",
222
+ "@smthrs/keys": "1.0.0-rc.7",
223
+ "@smthrs/canonical": "1.0.0-rc.7"
224
224
  },
225
225
  "devDependencies": {
226
226
  "@effect/language-service": "0.87.2",