@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 +13 -13
- package/docs/guides/diff-two-plans.md +158 -13
- package/package.json +4 -4
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://
|
|
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
|
|
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
|
|
39
|
-
and brings [`@smthrs/database`](https://
|
|
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
|
|
104
|
-
[quickstart](https://plan-store
|
|
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://
|
|
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
|
|
186
|
+
[`@smthrs/plan-store`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan-store/docs)'s.
|
|
187
187
|
|
|
188
|
-
[Troubleshooting](https://
|
|
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://
|
|
224
|
-
- [API reference](https://
|
|
225
|
-
- [`@smthrs/plan-store`](https://plan-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://
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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*
|
|
25
|
-
const after = yield*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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.
|
|
222
|
-
"@smthrs/keys": "1.0.0-rc.
|
|
223
|
-
"@smthrs/canonical": "1.0.0-rc.
|
|
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",
|