@smthrs/plan 1.0.0-rc.7 → 1.0.0-rc.9
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 +3 -1
- package/dist/cjs/Effects.d.ts +29 -11
- package/dist/cjs/Effects.d.ts.map +1 -1
- package/dist/cjs/Effects.js.map +2 -2
- package/dist/cjs/FileSet.d.ts +121 -51
- package/dist/cjs/FileSet.d.ts.map +1 -1
- package/dist/cjs/FileSet.js.map +2 -2
- package/dist/cjs/GraphBuildError.d.ts +2 -4
- package/dist/cjs/GraphBuildError.d.ts.map +1 -1
- package/dist/cjs/GraphBuildError.js.map +2 -2
- package/dist/cjs/KeyMaterial.d.ts +13 -8
- package/dist/cjs/KeyMaterial.d.ts.map +1 -1
- package/dist/cjs/KeyMaterial.js.map +2 -2
- package/dist/cjs/Plan.d.ts +72 -23
- package/dist/cjs/Plan.d.ts.map +1 -1
- package/dist/cjs/Plan.js +3 -0
- package/dist/cjs/Plan.js.map +2 -2
- package/dist/cjs/PlanDiff.d.ts +26 -7
- package/dist/cjs/PlanDiff.d.ts.map +1 -1
- package/dist/cjs/PlanDiff.js +29 -4
- package/dist/cjs/PlanDiff.js.map +2 -2
- package/dist/cjs/StepKey.d.ts +38 -17
- package/dist/cjs/StepKey.d.ts.map +1 -1
- package/dist/cjs/StepKey.js +24 -22
- package/dist/cjs/StepKey.js.map +2 -2
- package/dist/cjs/internal/node.d.ts.map +1 -1
- package/dist/esm/Effects.d.ts +29 -11
- package/dist/esm/Effects.d.ts.map +1 -1
- package/dist/esm/Effects.js +18 -7
- package/dist/esm/Effects.js.map +1 -1
- package/dist/esm/FileSet.d.ts +121 -51
- package/dist/esm/FileSet.d.ts.map +1 -1
- package/dist/esm/FileSet.js +97 -35
- package/dist/esm/FileSet.js.map +1 -1
- package/dist/esm/GraphBuildError.d.ts +2 -4
- package/dist/esm/GraphBuildError.d.ts.map +1 -1
- package/dist/esm/GraphBuildError.js +2 -3
- package/dist/esm/GraphBuildError.js.map +1 -1
- package/dist/esm/KeyMaterial.d.ts +13 -8
- package/dist/esm/KeyMaterial.d.ts.map +1 -1
- package/dist/esm/KeyMaterial.js +11 -4
- package/dist/esm/KeyMaterial.js.map +1 -1
- package/dist/esm/Plan.d.ts +72 -23
- package/dist/esm/Plan.d.ts.map +1 -1
- package/dist/esm/Plan.js +61 -16
- package/dist/esm/Plan.js.map +1 -1
- package/dist/esm/PlanDiff.d.ts +26 -7
- package/dist/esm/PlanDiff.d.ts.map +1 -1
- package/dist/esm/PlanDiff.js +55 -9
- package/dist/esm/PlanDiff.js.map +1 -1
- package/dist/esm/StepKey.d.ts +38 -17
- package/dist/esm/StepKey.d.ts.map +1 -1
- package/dist/esm/StepKey.js +41 -16
- package/dist/esm/StepKey.js.map +1 -1
- package/dist/esm/internal/node.d.ts.map +1 -1
- package/docs/README.md +3 -1
- package/docs/api.md +1 -1
- package/docs/guides/diff-two-plans.md +138 -74
- package/docs/installation.md +5 -1
- package/docs/quickstart.md +1 -1
- package/package.json +4 -4
- package/src/Effects.ts +29 -11
- package/src/FileSet.ts +121 -51
- package/src/GraphBuildError.ts +2 -4
- package/src/KeyMaterial.ts +13 -8
- package/src/Plan.ts +66 -23
- package/src/PlanDiff.ts +56 -11
- package/src/StepKey.ts +66 -40
|
@@ -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,
|
|
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, how a step whose positional id moved is paired by its key, and why the report is in no digest."
|
|
4
4
|
sidebar:
|
|
5
5
|
order: 5
|
|
6
6
|
---
|
|
@@ -17,10 +17,10 @@ add this package and `@smthrs/flow` at the version of `@smthrs/flows` you have,
|
|
|
17
17
|
plus the platform package:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install @smthrs/plan@1.0.0-rc.
|
|
20
|
+
npm install @smthrs/plan@1.0.0-rc.9 @smthrs/flow@1.0.0-rc.9 @effect/platform-node
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
The outputs below are real output from
|
|
23
|
+
The outputs below are real output from the release that install line names.
|
|
24
24
|
|
|
25
25
|
## Compare a re-plan
|
|
26
26
|
|
|
@@ -90,15 +90,16 @@ body, so that node re-keyed and the report says which field did it.
|
|
|
90
90
|
|
|
91
91
|
## The verdict is the key
|
|
92
92
|
|
|
93
|
-
Two nodes with the same
|
|
94
|
-
needs saying, and `diff` says nothing else. Four lists come
|
|
93
|
+
Two nodes with the same key are the same step, under the same id or a moved
|
|
94
|
+
one. Nothing else needs saying, and `diff` says nothing else. Four lists come
|
|
95
|
+
back:
|
|
95
96
|
|
|
96
|
-
| Field | Contents
|
|
97
|
-
| ----------- |
|
|
98
|
-
| `added` | Node ids
|
|
99
|
-
| `removed` | Node ids
|
|
100
|
-
| `rekeyed` | Nodes
|
|
101
|
-
| `unchanged` | Node ids
|
|
97
|
+
| Field | Contents |
|
|
98
|
+
| ----------- | --------------------------------------------------------------------- |
|
|
99
|
+
| `added` | Node ids in `next` that pair with no node in `previous`. |
|
|
100
|
+
| `removed` | Node ids in `previous` that pair with no node in `next`. |
|
|
101
|
+
| `rekeyed` | Nodes paired by id whose key moved, with `from`, `to`, and `changed`. |
|
|
102
|
+
| `unchanged` | Node ids in `next` paired with a node of the same key. |
|
|
102
103
|
|
|
103
104
|
## The attribution is for a human
|
|
104
105
|
|
|
@@ -126,90 +127,76 @@ on a re-keyed node as a bug report, not as noise.
|
|
|
126
127
|
## Node ids are positional
|
|
127
128
|
|
|
128
129
|
A node id is the node's structural address in the body, such as
|
|
129
|
-
`root.flow.andThen`, not the name of its step.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
`root.flow.andThen`, not the name of its step. Adding a step to a flow can move
|
|
131
|
+
the steps around it to new addresses while their keys stay the same. `diff`
|
|
132
|
+
therefore pairs nodes in three passes, each over the nodes no earlier pass
|
|
133
|
+
paired:
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
1. The same id with the same key: unchanged.
|
|
136
|
+
2. The same key under a different id: the same step at a new address, reported
|
|
137
|
+
as unchanged under its id in `next`.
|
|
138
|
+
3. The same id with a different key: re-keyed.
|
|
139
|
+
|
|
140
|
+
Whatever is left in `next` is `added`, and whatever is left in `previous` is
|
|
141
|
+
`removed`. `added`, `rekeyed`, and `unchanged` name nodes by their id in
|
|
142
|
+
`next`; `removed` names nodes by their id in `previous`.
|
|
143
|
+
|
|
144
|
+
Create `inserted.ts`. It compiles two steps, then the same two steps with a
|
|
145
|
+
third inserted before them, so every id shifts by one:
|
|
136
146
|
|
|
137
147
|
```ts
|
|
138
148
|
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
|
|
139
|
-
import
|
|
140
|
-
import { Node } from "@smthrs/plan"
|
|
149
|
+
import * as KeyMaterial from "@smthrs/plan/KeyMaterial"
|
|
141
150
|
import * as Plan from "@smthrs/plan/Plan"
|
|
142
151
|
import * as PlanDiff from "@smthrs/plan/PlanDiff"
|
|
143
152
|
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
153
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
Node.bindPlanned((plan) => Test.call({ plan })),
|
|
163
|
-
Node.bindPlanned((result) => Report.call({ result }))
|
|
164
|
-
)
|
|
154
|
+
/** A step whose id is its position, as a flow's structural ids are. */
|
|
155
|
+
const step = (position: number, action: string): Plan.NodeDraft => ({
|
|
156
|
+
id: `step-${position}`,
|
|
157
|
+
material: {
|
|
158
|
+
version: KeyMaterial.version,
|
|
159
|
+
kind: "sealed",
|
|
160
|
+
body: { action },
|
|
161
|
+
inputs: [],
|
|
162
|
+
layers: [],
|
|
163
|
+
capabilities: []
|
|
164
|
+
},
|
|
165
|
+
effects: { reads: [], writes: [], boundaryMode: "hard" }
|
|
165
166
|
})
|
|
166
167
|
|
|
167
|
-
const
|
|
168
|
-
Plan.compile({
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return typeof body === "object" && body !== null && "action" in body ? String(body.action) : undefined
|
|
174
|
-
}
|
|
168
|
+
const compile = (actions: ReadonlyArray<string>) =>
|
|
169
|
+
Plan.compile({
|
|
170
|
+
planId: "checks-1",
|
|
171
|
+
flow: "example/Checks",
|
|
172
|
+
nodes: actions.map((action, index) => step(index, action))
|
|
173
|
+
})
|
|
175
174
|
|
|
176
175
|
const program = Effect.gen(function*() {
|
|
177
|
-
const previous = yield*
|
|
178
|
-
const next = yield*
|
|
179
|
-
|
|
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
|
-
}
|
|
176
|
+
const previous = yield* compile(["lint", "test"])
|
|
177
|
+
const next = yield* compile(["format", "lint", "test"])
|
|
178
|
+
console.log(PlanDiff.diff(previous, next))
|
|
190
179
|
}).pipe(Effect.provide(NodeCrypto.layer))
|
|
191
180
|
|
|
192
181
|
await Effect.runPromise(program)
|
|
193
182
|
```
|
|
194
183
|
|
|
195
184
|
```bash
|
|
196
|
-
node
|
|
185
|
+
node inserted.ts
|
|
197
186
|
```
|
|
198
187
|
|
|
199
188
|
```text
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
189
|
+
{
|
|
190
|
+
added: [ 'step-0' ],
|
|
191
|
+
removed: [],
|
|
192
|
+
rekeyed: [],
|
|
193
|
+
unchanged: [ 'step-1', 'step-2' ]
|
|
194
|
+
}
|
|
206
195
|
```
|
|
207
196
|
|
|
208
|
-
Only `
|
|
209
|
-
and `
|
|
210
|
-
|
|
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.
|
|
197
|
+
Only the inserted `format` step is new. `lint` and `test` moved from `step-0`
|
|
198
|
+
and `step-1` to `step-1` and `step-2` with the keys they had, so they come back
|
|
199
|
+
as `unchanged`.
|
|
213
200
|
|
|
214
201
|
## Comparison uses the key's own JSON semantics
|
|
215
202
|
|
|
@@ -229,10 +216,87 @@ The pairing this is built for is a re-plan of the same flow: compile the flow
|
|
|
229
216
|
again against the current source and compare the result with the plan an
|
|
230
217
|
operator last saw. `added` and `removed` are the shape change, `rekeyed` is the
|
|
231
218
|
work that will not come from cache, and `unchanged` is everything a cache hit
|
|
232
|
-
will skip. That reading holds
|
|
233
|
-
|
|
219
|
+
will skip. That reading holds when a step is added or removed and ids move,
|
|
220
|
+
because `diff` pairs a moved step by its key, as
|
|
234
221
|
[Node ids are positional](#node-ids-are-positional) shows.
|
|
235
222
|
|
|
223
|
+
On a body built with `Flow.make`, `added` and `rekeyed` also list structural
|
|
224
|
+
nodes: each `AndThen` that sequences steps, and the `FlowCall` at `root`. Their
|
|
225
|
+
keys fold their children's keys, so they move whenever a child does, and they
|
|
226
|
+
run no work. Count steps as the entries whose material body names an `action`.
|
|
227
|
+
|
|
228
|
+
Create `replan.ts`. It appends a `Summarize` step to a two-step review and
|
|
229
|
+
compares the two plans:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
|
|
233
|
+
import { Action, Flow, Graph } from "@smthrs/flow"
|
|
234
|
+
import { Node } from "@smthrs/plan"
|
|
235
|
+
import * as Plan from "@smthrs/plan/Plan"
|
|
236
|
+
import * as PlanDiff from "@smthrs/plan/PlanDiff"
|
|
237
|
+
import * as Effect from "effect/Effect"
|
|
238
|
+
import * as Schema from "effect/Schema"
|
|
239
|
+
|
|
240
|
+
const ReadPr = Action.make("review/ReadPr", { payload: { pr: Schema.Number }, success: Schema.String })
|
|
241
|
+
const RunTests = Action.make("review/RunTests", { payload: { diff: Schema.String }, success: Schema.String })
|
|
242
|
+
const Summarize = Action.make("review/Summarize", { payload: { report: Schema.String }, success: Schema.String })
|
|
243
|
+
|
|
244
|
+
const review = (summarize: boolean) =>
|
|
245
|
+
Flow.make("review/Review", {
|
|
246
|
+
payload: { pr: Schema.Number },
|
|
247
|
+
success: Schema.String,
|
|
248
|
+
body: (payload) => {
|
|
249
|
+
const tested = ReadPr.call(payload).pipe(Node.bindPlanned((diff) => RunTests.call({ diff })))
|
|
250
|
+
return summarize ? tested.pipe(Node.bindPlanned((report) => Summarize.call({ report }))) : tested
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const compile = (summarize: boolean) =>
|
|
255
|
+
Plan.compile({
|
|
256
|
+
planId: "review-4821",
|
|
257
|
+
flow: "review/Review",
|
|
258
|
+
nodes: Graph.drafts(Graph.build(review(summarize), { pr: 4821 }))
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
/** A step is a node whose material body names an action; the rest only compose steps. */
|
|
262
|
+
const isStep = (node: Plan.PlanNode) => {
|
|
263
|
+
const body = node.material.body
|
|
264
|
+
return typeof body === "object" && body !== null && "action" in body
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const program = Effect.gen(function*() {
|
|
268
|
+
const previous = yield* compile(false)
|
|
269
|
+
const next = yield* compile(true)
|
|
270
|
+
const changes = PlanDiff.diff(previous, next)
|
|
271
|
+
const steps = new Set(next.nodes.filter(isStep).map((node) => node.id))
|
|
272
|
+
console.log({
|
|
273
|
+
added: changes.added,
|
|
274
|
+
rekeyed: changes.rekeyed.map((entry) => entry.id),
|
|
275
|
+
addedSteps: changes.added.filter((id) => steps.has(id)),
|
|
276
|
+
rekeyedSteps: changes.rekeyed.filter((entry) => steps.has(entry.id)).map((entry) => entry.id)
|
|
277
|
+
})
|
|
278
|
+
}).pipe(Effect.provide(NodeCrypto.layer))
|
|
279
|
+
|
|
280
|
+
await Effect.runPromise(program)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
node replan.ts
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
```text
|
|
288
|
+
{
|
|
289
|
+
added: [ 'root.flow.andThen', 'root.flow.then' ],
|
|
290
|
+
rekeyed: [ 'root.flow', 'root' ],
|
|
291
|
+
addedSteps: [ 'root.flow.then' ],
|
|
292
|
+
rekeyedSteps: []
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
One step was added: `Summarize`, at `root.flow.then`. `ReadPr` and `RunTests`
|
|
297
|
+
moved under the new `AndThen` with the keys they had, so they are `unchanged`.
|
|
298
|
+
The other three entries are structure: two `AndThen` nodes and the `FlowCall`.
|
|
299
|
+
|
|
236
300
|
The command line prints the same comparison through
|
|
237
301
|
[`smthrs flow plan`](/cli/flow), and the plan card it renders is what
|
|
238
302
|
[`smthrs approvals approve`](/cli/approvals) binds a decision to.
|
package/docs/installation.md
CHANGED
|
@@ -13,7 +13,11 @@ sidebar:
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install @smthrs/plan@next effect
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `next` tag carries the 1.0 release candidates.
|
|
17
21
|
|
|
18
22
|
That is enough to build node graphs, compile plans, and diff them. Compiling
|
|
19
23
|
asks for Effect's `Crypto` service, which a platform package supplies:
|
package/docs/quickstart.md
CHANGED
|
@@ -17,7 +17,7 @@ Nothing here executes a node or writes a row. A plan is inert: persisting one is
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
19
19
|
- Node.js 26.4.0 or later.
|
|
20
|
-
- A
|
|
20
|
+
- A project with the packages installed: `npm install @smthrs/plan@next @effect/platform-node effect`.
|
|
21
21
|
|
|
22
22
|
`@effect/platform-node` supplies Effect's `Crypto` service, which is the only
|
|
23
23
|
thing compiling a plan asks for.
|
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.9",
|
|
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.9",
|
|
222
|
+
"@smthrs/keys": "1.0.0-rc.9",
|
|
223
|
+
"@smthrs/canonical": "1.0.0-rc.9"
|
|
224
224
|
},
|
|
225
225
|
"devDependencies": {
|
|
226
226
|
"@effect/language-service": "0.87.2",
|
package/src/Effects.ts
CHANGED
|
@@ -139,17 +139,21 @@ export {
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* A normalized description of the resources a flow or step may read and
|
|
142
|
-
* write.
|
|
142
|
+
* write. Build one with {@link make}.
|
|
143
143
|
*
|
|
144
144
|
* @category models
|
|
145
145
|
* @since 0.0.0
|
|
146
|
-
* @slop
|
|
147
146
|
*/
|
|
148
147
|
export interface Declaration {
|
|
148
|
+
/** Readable paths or envelope patterns, sorted and duplicate-free. */
|
|
149
149
|
readonly reads: ReadonlyArray<string>
|
|
150
|
+
/** Writable paths or envelope patterns, sorted and duplicate-free. */
|
|
150
151
|
readonly writes: ReadonlyArray<string>
|
|
152
|
+
/** `hermetic` or `expected` enforcement. */
|
|
151
153
|
readonly mode: Mode
|
|
154
|
+
/** What a plan does when another node writes an overlapping path. */
|
|
152
155
|
readonly onConflict: ConflictStrategy
|
|
156
|
+
/** The effect tier. Absent means `sealed`. */
|
|
153
157
|
readonly tier?: Tier | undefined
|
|
154
158
|
}
|
|
155
159
|
|
|
@@ -163,7 +167,6 @@ export interface Declaration {
|
|
|
163
167
|
*
|
|
164
168
|
* @category models
|
|
165
169
|
* @since 0.0.0
|
|
166
|
-
* @slop
|
|
167
170
|
*/
|
|
168
171
|
export interface MakeOptions {
|
|
169
172
|
readonly reads: Iterable<string>
|
|
@@ -176,9 +179,13 @@ export interface MakeOptions {
|
|
|
176
179
|
/**
|
|
177
180
|
* A result of checking that a step declaration narrows a flow envelope.
|
|
178
181
|
*
|
|
182
|
+
* On failure, `code` names the first rule broken, checked in this order:
|
|
183
|
+
* `effect_outside_envelope` (with the uncovered read and write paths, sorted),
|
|
184
|
+
* `effect_mode_widening`, then `effect_tier_widening` (both with empty
|
|
185
|
+
* `paths`).
|
|
186
|
+
*
|
|
179
187
|
* @category models
|
|
180
188
|
* @since 0.0.0
|
|
181
|
-
* @slop
|
|
182
189
|
*/
|
|
183
190
|
export type NarrowResult =
|
|
184
191
|
| { readonly ok: true }
|
|
@@ -191,11 +198,14 @@ export type NarrowResult =
|
|
|
191
198
|
const normalize = (paths: Iterable<string>): ReadonlyArray<string> => [...new Set(paths)].sort()
|
|
192
199
|
|
|
193
200
|
/**
|
|
194
|
-
* Constructs a deterministic effect declaration
|
|
201
|
+
* Constructs a deterministic effect declaration: `reads` and `writes` are
|
|
202
|
+
* collected, deduplicated, and sorted; an absent `tier` is omitted. Paths are
|
|
203
|
+
* not validated or rewritten.
|
|
195
204
|
*
|
|
205
|
+
* @param input - The paths, mode, conflict strategy, and optional tier.
|
|
206
|
+
* @returns The normalized declaration.
|
|
196
207
|
* @category constructors
|
|
197
208
|
* @since 0.0.0
|
|
198
|
-
* @slop
|
|
199
209
|
*/
|
|
200
210
|
export const make = (input: MakeOptions): Declaration => ({
|
|
201
211
|
reads: normalize(input.reads),
|
|
@@ -216,9 +226,11 @@ export const make = (input: MakeOptions): Declaration => ({
|
|
|
216
226
|
* covered, so {@link narrow} reports it as `effect_outside_envelope` rather
|
|
217
227
|
* than silently accepting an escape.
|
|
218
228
|
*
|
|
229
|
+
* @param envelope - One envelope entry.
|
|
230
|
+
* @param path - One declared path.
|
|
231
|
+
* @returns `true` when the entry covers the path.
|
|
219
232
|
* @category predicates
|
|
220
233
|
* @since 0.0.0
|
|
221
|
-
* @slop
|
|
222
234
|
*/
|
|
223
235
|
export const covers = (envelope: string, path: string): boolean => {
|
|
224
236
|
if (Index.hasDotSegment(path)) return false
|
|
@@ -239,9 +251,11 @@ export const covers = (envelope: string, path: string): boolean => {
|
|
|
239
251
|
* patterns nest. A graph build prepares each envelope once for every node it
|
|
240
252
|
* encloses.
|
|
241
253
|
*
|
|
254
|
+
* @param envelope - The flow's declaration.
|
|
255
|
+
* @param step - The step's declaration.
|
|
256
|
+
* @returns `{ ok: true }`, or the first rule the step breaks.
|
|
242
257
|
* @category validation
|
|
243
258
|
* @since 0.0.0
|
|
244
|
-
* @slop
|
|
245
259
|
*/
|
|
246
260
|
export const narrow = (envelope: Declaration, step: Declaration): NarrowResult =>
|
|
247
261
|
Index.narrowPrepared(Index.prepareEnvelope(envelope), step)
|
|
@@ -263,9 +277,11 @@ export const narrow = (envelope: Declaration, step: Declaration): NarrowResult =
|
|
|
263
277
|
* ranks inside its interval, so the cost is linear in the two declarations
|
|
264
278
|
* plus the matches, whatever the paths' lengths or how many patterns nest.
|
|
265
279
|
*
|
|
280
|
+
* @param a - One declaration.
|
|
281
|
+
* @param b - The other declaration.
|
|
282
|
+
* @returns The shared write paths; empty when the writes are disjoint.
|
|
266
283
|
* @category analysis
|
|
267
284
|
* @since 0.0.0
|
|
268
|
-
* @slop
|
|
269
285
|
*/
|
|
270
286
|
export const overlaps = (a: Declaration, b: Declaration): ReadonlyArray<string> => {
|
|
271
287
|
const indexed = Index.indexPaths([a.writes, b.writes])
|
|
@@ -274,11 +290,13 @@ export const overlaps = (a: Declaration, b: Declaration): ReadonlyArray<string>
|
|
|
274
290
|
}
|
|
275
291
|
|
|
276
292
|
/**
|
|
277
|
-
* Returns a sealed, hermetic copy of an effect declaration
|
|
293
|
+
* Returns a sealed, hermetic copy of an effect declaration, normalized by
|
|
294
|
+
* {@link make}. Paths and `onConflict` are kept.
|
|
278
295
|
*
|
|
296
|
+
* @param declaration - The declaration to copy.
|
|
297
|
+
* @returns The copy with `mode: "hermetic"` and `tier: "sealed"`.
|
|
279
298
|
* @category constructors
|
|
280
299
|
* @since 0.0.0
|
|
281
|
-
* @slop
|
|
282
300
|
*/
|
|
283
301
|
export const sealed = (declaration: Declaration): Declaration =>
|
|
284
302
|
make({ ...declaration, mode: "hermetic", tier: "sealed" })
|