@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.
Files changed (68) hide show
  1. package/README.md +3 -1
  2. package/dist/cjs/Effects.d.ts +29 -11
  3. package/dist/cjs/Effects.d.ts.map +1 -1
  4. package/dist/cjs/Effects.js.map +2 -2
  5. package/dist/cjs/FileSet.d.ts +121 -51
  6. package/dist/cjs/FileSet.d.ts.map +1 -1
  7. package/dist/cjs/FileSet.js.map +2 -2
  8. package/dist/cjs/GraphBuildError.d.ts +2 -4
  9. package/dist/cjs/GraphBuildError.d.ts.map +1 -1
  10. package/dist/cjs/GraphBuildError.js.map +2 -2
  11. package/dist/cjs/KeyMaterial.d.ts +13 -8
  12. package/dist/cjs/KeyMaterial.d.ts.map +1 -1
  13. package/dist/cjs/KeyMaterial.js.map +2 -2
  14. package/dist/cjs/Plan.d.ts +72 -23
  15. package/dist/cjs/Plan.d.ts.map +1 -1
  16. package/dist/cjs/Plan.js +3 -0
  17. package/dist/cjs/Plan.js.map +2 -2
  18. package/dist/cjs/PlanDiff.d.ts +26 -7
  19. package/dist/cjs/PlanDiff.d.ts.map +1 -1
  20. package/dist/cjs/PlanDiff.js +29 -4
  21. package/dist/cjs/PlanDiff.js.map +2 -2
  22. package/dist/cjs/StepKey.d.ts +38 -17
  23. package/dist/cjs/StepKey.d.ts.map +1 -1
  24. package/dist/cjs/StepKey.js +24 -22
  25. package/dist/cjs/StepKey.js.map +2 -2
  26. package/dist/cjs/internal/node.d.ts.map +1 -1
  27. package/dist/esm/Effects.d.ts +29 -11
  28. package/dist/esm/Effects.d.ts.map +1 -1
  29. package/dist/esm/Effects.js +18 -7
  30. package/dist/esm/Effects.js.map +1 -1
  31. package/dist/esm/FileSet.d.ts +121 -51
  32. package/dist/esm/FileSet.d.ts.map +1 -1
  33. package/dist/esm/FileSet.js +97 -35
  34. package/dist/esm/FileSet.js.map +1 -1
  35. package/dist/esm/GraphBuildError.d.ts +2 -4
  36. package/dist/esm/GraphBuildError.d.ts.map +1 -1
  37. package/dist/esm/GraphBuildError.js +2 -3
  38. package/dist/esm/GraphBuildError.js.map +1 -1
  39. package/dist/esm/KeyMaterial.d.ts +13 -8
  40. package/dist/esm/KeyMaterial.d.ts.map +1 -1
  41. package/dist/esm/KeyMaterial.js +11 -4
  42. package/dist/esm/KeyMaterial.js.map +1 -1
  43. package/dist/esm/Plan.d.ts +72 -23
  44. package/dist/esm/Plan.d.ts.map +1 -1
  45. package/dist/esm/Plan.js +61 -16
  46. package/dist/esm/Plan.js.map +1 -1
  47. package/dist/esm/PlanDiff.d.ts +26 -7
  48. package/dist/esm/PlanDiff.d.ts.map +1 -1
  49. package/dist/esm/PlanDiff.js +55 -9
  50. package/dist/esm/PlanDiff.js.map +1 -1
  51. package/dist/esm/StepKey.d.ts +38 -17
  52. package/dist/esm/StepKey.d.ts.map +1 -1
  53. package/dist/esm/StepKey.js +41 -16
  54. package/dist/esm/StepKey.js.map +1 -1
  55. package/dist/esm/internal/node.d.ts.map +1 -1
  56. package/docs/README.md +3 -1
  57. package/docs/api.md +1 -1
  58. package/docs/guides/diff-two-plans.md +138 -74
  59. package/docs/installation.md +5 -1
  60. package/docs/quickstart.md +1 -1
  61. package/package.json +4 -4
  62. package/src/Effects.ts +29 -11
  63. package/src/FileSet.ts +121 -51
  64. package/src/GraphBuildError.ts +2 -4
  65. package/src/KeyMaterial.ts +13 -8
  66. package/src/Plan.ts +66 -23
  67. package/src/PlanDiff.ts +56 -11
  68. 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, why node ids are positional, 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, 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.7 @smthrs/flow@1.0.0-rc.7 @effect/platform-node
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 1.0.0-rc.6.
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 id and the same key are the same step. Nothing else
94
- needs saying, and `diff` says nothing else. Four lists come back:
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 present in `next` and absent from `previous`. |
99
- | `removed` | Node ids present in `previous` and absent from `next`. |
100
- | `rekeyed` | Nodes present in both whose key moved, with `from`, `to`, and `changed`. |
101
- | `unchanged` | Node ids present in both with the same key. |
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. 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.
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
- 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:
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 { Action, Flow, Graph } from "@smthrs/flow"
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
- 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
- )
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 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
- }
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* 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
- }
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 positional.ts
185
+ node inserted.ts
197
186
  ```
198
187
 
199
188
  ```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
189
+ {
190
+ added: [ 'step-0' ],
191
+ removed: [],
192
+ rekeyed: [],
193
+ unchanged: [ 'step-1', 'step-2' ]
194
+ }
206
195
  ```
207
196
 
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.
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 while the shape stays put. Once a step is added
233
- or removed, ids move, so read the work by key as
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.
@@ -13,7 +13,11 @@ sidebar:
13
13
 
14
14
  ## Install
15
15
 
16
- Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
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:
@@ -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 package with the dependencies installed. Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
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.7",
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.7",
222
- "@smthrs/keys": "1.0.0-rc.7",
223
- "@smthrs/canonical": "1.0.0-rc.7"
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" })