@smthrs/plan 0.0.0-stage → 1.0.0-rc.3
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/CHANGELOG.md +183 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/dist/cjs/CachePolicy.d.ts +131 -0
- package/dist/cjs/CachePolicy.d.ts.map +1 -0
- package/dist/cjs/CachePolicy.js +50 -0
- package/dist/cjs/CachePolicy.js.map +7 -0
- package/dist/cjs/Effects.d.ts +240 -0
- package/dist/cjs/Effects.d.ts.map +1 -0
- package/dist/cjs/Effects.js +76 -0
- package/dist/cjs/Effects.js.map +7 -0
- package/dist/cjs/FileSet.d.ts +288 -0
- package/dist/cjs/FileSet.d.ts.map +1 -0
- package/dist/cjs/FileSet.js +191 -0
- package/dist/cjs/FileSet.js.map +7 -0
- package/dist/cjs/GraphBuildError.d.ts +122 -0
- package/dist/cjs/GraphBuildError.d.ts.map +1 -0
- package/dist/cjs/GraphBuildError.js +71 -0
- package/dist/cjs/GraphBuildError.js.map +7 -0
- package/dist/cjs/KeyMaterial.d.ts +102 -0
- package/dist/cjs/KeyMaterial.d.ts.map +1 -0
- package/dist/cjs/KeyMaterial.js +64 -0
- package/dist/cjs/KeyMaterial.js.map +7 -0
- package/dist/cjs/Node.d.ts +606 -0
- package/dist/cjs/Node.d.ts.map +1 -0
- package/dist/cjs/Node.js +207 -0
- package/dist/cjs/Node.js.map +7 -0
- package/dist/cjs/Placement.d.ts +117 -0
- package/dist/cjs/Placement.d.ts.map +1 -0
- package/dist/cjs/Placement.js +46 -0
- package/dist/cjs/Placement.js.map +7 -0
- package/dist/cjs/Plan.d.ts +440 -0
- package/dist/cjs/Plan.d.ts.map +1 -0
- package/dist/cjs/Plan.js +508 -0
- package/dist/cjs/Plan.js.map +7 -0
- package/dist/cjs/PlanDiff.d.ts +58 -0
- package/dist/cjs/PlanDiff.d.ts.map +1 -0
- package/dist/cjs/PlanDiff.js +124 -0
- package/dist/cjs/PlanDiff.js.map +7 -0
- package/dist/cjs/Planned.d.ts +121 -0
- package/dist/cjs/Planned.d.ts.map +1 -0
- package/dist/cjs/Planned.js +72 -0
- package/dist/cjs/Planned.js.map +7 -0
- package/dist/cjs/Repetition.d.ts +31 -0
- package/dist/cjs/Repetition.d.ts.map +1 -0
- package/dist/cjs/Repetition.js +36 -0
- package/dist/cjs/Repetition.js.map +7 -0
- package/dist/cjs/Scheduling.d.ts +89 -0
- package/dist/cjs/Scheduling.d.ts.map +1 -0
- package/dist/cjs/Scheduling.js +76 -0
- package/dist/cjs/Scheduling.js.map +7 -0
- package/dist/cjs/StepKey.d.ts +333 -0
- package/dist/cjs/StepKey.d.ts.map +1 -0
- package/dist/cjs/StepKey.js +311 -0
- package/dist/cjs/StepKey.js.map +7 -0
- package/dist/cjs/index.d.ts +91 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +59 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ConflictAnnotation.d.ts +59 -0
- package/dist/cjs/internal/ConflictAnnotation.d.ts.map +1 -0
- package/dist/cjs/internal/ConflictAnnotation.js +247 -0
- package/dist/cjs/internal/ConflictAnnotation.js.map +7 -0
- package/dist/cjs/internal/EffectCandidates.d.ts +19 -0
- package/dist/cjs/internal/EffectCandidates.d.ts.map +1 -0
- package/dist/cjs/internal/EffectCandidates.js +92 -0
- package/dist/cjs/internal/EffectCandidates.js.map +7 -0
- package/dist/cjs/internal/JsonMirror.d.ts +34 -0
- package/dist/cjs/internal/JsonMirror.d.ts.map +1 -0
- package/dist/cjs/internal/JsonMirror.js +195 -0
- package/dist/cjs/internal/JsonMirror.js.map +7 -0
- package/dist/cjs/internal/effects.d.ts +245 -0
- package/dist/cjs/internal/effects.d.ts.map +1 -0
- package/dist/cjs/internal/effects.js +290 -0
- package/dist/cjs/internal/effects.js.map +7 -0
- package/dist/cjs/internal/node.d.ts +526 -0
- package/dist/cjs/internal/node.d.ts.map +1 -0
- package/dist/cjs/internal/node.js +312 -0
- package/dist/cjs/internal/node.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/PlanFixtures.d.ts +127 -0
- package/dist/cjs/test/PlanFixtures.d.ts.map +1 -0
- package/dist/cjs/test/PlanFixtures.js +59 -0
- package/dist/cjs/test/PlanFixtures.js.map +7 -0
- package/dist/esm/CachePolicy.d.ts +131 -0
- package/dist/esm/CachePolicy.d.ts.map +1 -0
- package/dist/esm/CachePolicy.js +117 -0
- package/dist/esm/CachePolicy.js.map +1 -0
- package/dist/esm/Effects.d.ts +240 -0
- package/dist/esm/Effects.d.ts.map +1 -0
- package/dist/esm/Effects.js +167 -0
- package/dist/esm/Effects.js.map +1 -0
- package/dist/esm/FileSet.d.ts +288 -0
- package/dist/esm/FileSet.d.ts.map +1 -0
- package/dist/esm/FileSet.js +324 -0
- package/dist/esm/FileSet.js.map +1 -0
- package/dist/esm/GraphBuildError.d.ts +122 -0
- package/dist/esm/GraphBuildError.d.ts.map +1 -0
- package/dist/esm/GraphBuildError.js +146 -0
- package/dist/esm/GraphBuildError.js.map +1 -0
- package/dist/esm/KeyMaterial.d.ts +102 -0
- package/dist/esm/KeyMaterial.d.ts.map +1 -0
- package/dist/esm/KeyMaterial.js +85 -0
- package/dist/esm/KeyMaterial.js.map +1 -0
- package/dist/esm/Node.d.ts +606 -0
- package/dist/esm/Node.d.ts.map +1 -0
- package/dist/esm/Node.js +521 -0
- package/dist/esm/Node.js.map +1 -0
- package/dist/esm/Placement.d.ts +117 -0
- package/dist/esm/Placement.d.ts.map +1 -0
- package/dist/esm/Placement.js +65 -0
- package/dist/esm/Placement.js.map +1 -0
- package/dist/esm/Plan.d.ts +440 -0
- package/dist/esm/Plan.d.ts.map +1 -0
- package/dist/esm/Plan.js +673 -0
- package/dist/esm/Plan.js.map +1 -0
- package/dist/esm/PlanDiff.d.ts +58 -0
- package/dist/esm/PlanDiff.d.ts.map +1 -0
- package/dist/esm/PlanDiff.js +159 -0
- package/dist/esm/PlanDiff.js.map +1 -0
- package/dist/esm/Planned.d.ts +121 -0
- package/dist/esm/Planned.d.ts.map +1 -0
- package/dist/esm/Planned.js +147 -0
- package/dist/esm/Planned.js.map +1 -0
- package/dist/esm/Repetition.d.ts +31 -0
- package/dist/esm/Repetition.d.ts.map +1 -0
- package/dist/esm/Repetition.js +24 -0
- package/dist/esm/Repetition.js.map +1 -0
- package/dist/esm/Scheduling.d.ts +89 -0
- package/dist/esm/Scheduling.d.ts.map +1 -0
- package/dist/esm/Scheduling.js +82 -0
- package/dist/esm/Scheduling.js.map +1 -0
- package/dist/esm/StepKey.d.ts +333 -0
- package/dist/esm/StepKey.d.ts.map +1 -0
- package/dist/esm/StepKey.js +478 -0
- package/dist/esm/StepKey.js.map +1 -0
- package/dist/esm/index.d.ts +91 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +91 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ConflictAnnotation.d.ts +59 -0
- package/dist/esm/internal/ConflictAnnotation.d.ts.map +1 -0
- package/dist/esm/internal/ConflictAnnotation.js +332 -0
- package/dist/esm/internal/ConflictAnnotation.js.map +1 -0
- package/dist/esm/internal/EffectCandidates.d.ts +19 -0
- package/dist/esm/internal/EffectCandidates.d.ts.map +1 -0
- package/dist/esm/internal/EffectCandidates.js +86 -0
- package/dist/esm/internal/EffectCandidates.js.map +1 -0
- package/dist/esm/internal/JsonMirror.d.ts +34 -0
- package/dist/esm/internal/JsonMirror.d.ts.map +1 -0
- package/dist/esm/internal/JsonMirror.js +204 -0
- package/dist/esm/internal/JsonMirror.js.map +1 -0
- package/dist/esm/internal/effects.d.ts +245 -0
- package/dist/esm/internal/effects.d.ts.map +1 -0
- package/dist/esm/internal/effects.js +459 -0
- package/dist/esm/internal/effects.js.map +1 -0
- package/dist/esm/internal/node.d.ts +526 -0
- package/dist/esm/internal/node.d.ts.map +1 -0
- package/dist/esm/internal/node.js +564 -0
- package/dist/esm/internal/node.js.map +1 -0
- package/dist/esm/test/PlanFixtures.d.ts +127 -0
- package/dist/esm/test/PlanFixtures.d.ts.map +1 -0
- package/dist/esm/test/PlanFixtures.js +60 -0
- package/dist/esm/test/PlanFixtures.js.map +1 -0
- package/docs/README.md +165 -0
- package/docs/api.md +1475 -0
- package/docs/concepts/authoring-ast.md +203 -0
- package/docs/concepts/effects-and-conflicts.md +135 -0
- package/docs/concepts/plan-value.md +168 -0
- package/docs/concepts/step-keys.md +179 -0
- package/docs/guides/author-a-node-graph.md +182 -0
- package/docs/guides/compile-a-plan.md +146 -0
- package/docs/guides/declare-file-effects.md +143 -0
- package/docs/guides/diff-two-plans.md +101 -0
- package/docs/installation.md +71 -0
- package/docs/quickstart.md +128 -0
- package/docs/testing.md +73 -0
- package/docs/troubleshooting.md +314 -0
- package/package.json +254 -3
- package/src/CachePolicy.ts +143 -0
- package/src/Effects.ts +284 -0
- package/src/FileSet.ts +411 -0
- package/src/GraphBuildError.ts +158 -0
- package/src/KeyMaterial.ts +106 -0
- package/src/Node.ts +871 -0
- package/src/Placement.ts +99 -0
- package/src/Plan.ts +868 -0
- package/src/PlanDiff.ts +199 -0
- package/src/Planned.ts +208 -0
- package/src/Repetition.ts +33 -0
- package/src/Scheduling.ts +154 -0
- package/src/StepKey.ts +710 -0
- package/src/index.ts +103 -0
- package/src/internal/ConflictAnnotation.ts +374 -0
- package/src/internal/EffectCandidates.ts +89 -0
- package/src/internal/JsonMirror.ts +255 -0
- package/src/internal/effects.ts +543 -0
- package/src/internal/node.ts +858 -0
- package/src/test/PlanFixtures.ts +76 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Step keys"
|
|
3
|
+
description: "How key material becomes a step key: sealed content keys, run-local ordinal keys, dependency substitution, and the collisions the compiler is built to refuse."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A step key identifies a declaration or a durable execution. It is not, by
|
|
9
|
+
itself, permission to reuse a result across runs. `StepKey` produces these
|
|
10
|
+
identities from the declared `KeyMaterial`.
|
|
11
|
+
|
|
12
|
+
The derivations answer different questions:
|
|
13
|
+
|
|
14
|
+
- `StepKey.planIdentity` produces the **plan key** for every effect tier: the key a node is
|
|
15
|
+
_identified_ by, computed at plan time from the resolved keys of its
|
|
16
|
+
dependencies.
|
|
17
|
+
- `StepKey.dispatchIdentity` produces a sealed **dispatch key**: the key a run is
|
|
18
|
+
_cached_ under, computed at execution time from the settled content its
|
|
19
|
+
dependencies produced.
|
|
20
|
+
|
|
21
|
+
- `StepKey.ordinal` produces a run-local execution key for non-cacheable work.
|
|
22
|
+
The plan scheduler scopes it by run, plan, structural node, and declaration
|
|
23
|
+
fingerprint, so repeated identical effects do not collapse into one action.
|
|
24
|
+
|
|
25
|
+
Sealed plan keys still use `StepKey.fromKeyMaterial` and retain the existing
|
|
26
|
+
content-key format. Other tiers use a distinct `plan-declaration` namespace
|
|
27
|
+
that binds approvals without claiming cacheability.
|
|
28
|
+
|
|
29
|
+
## Key material
|
|
30
|
+
|
|
31
|
+
`KeyMaterial.KeyMaterial` is everything that can change a node's result:
|
|
32
|
+
|
|
33
|
+
| Field | What it contributes |
|
|
34
|
+
| ------------------ | ------------------------------------------------------------------------------------- |
|
|
35
|
+
| `version` | `KeyMaterial.version`. Folded into every hashed body, so a bump re-keys everything. |
|
|
36
|
+
| `kind` | The tier: `sealed`, `compensable`, or `irreversible`. |
|
|
37
|
+
| `nondeterministic` | Optional. Absence claims determinism; only the explicit declaration changes identity. |
|
|
38
|
+
| `body` | The node's own opaque declaration. |
|
|
39
|
+
| `inputs` | Ordered `InputRef` values: what this node consumes. |
|
|
40
|
+
| `layers` | The composition identity the step runs under. |
|
|
41
|
+
| `capabilities` | The authority the step declares. |
|
|
42
|
+
| `effects` | Opaque here: canonically serialized, never interpreted. |
|
|
43
|
+
| `placement` | Opaque here, for the same reason. |
|
|
44
|
+
|
|
45
|
+
Keeping `effects` and `placement` opaque is what keeps the key compiler
|
|
46
|
+
independent of whatever the flow builder decides an effect declaration looks
|
|
47
|
+
like. `Plan.compile` is stricter than the compiler underneath it: it decodes
|
|
48
|
+
`NodeDraft.effects` through `NodeEffects` and writes the result into
|
|
49
|
+
`material.effects`, replacing anything a caller put there. That makes the draft
|
|
50
|
+
declaration the single derivation point for effect identity, so a node's key
|
|
51
|
+
cannot disagree with the effects its conflict annotations and approval payload
|
|
52
|
+
were computed from.
|
|
53
|
+
|
|
54
|
+
## Input references
|
|
55
|
+
|
|
56
|
+
An `InputRef` is one of three tagged variants:
|
|
57
|
+
|
|
58
|
+
| Variant | Meaning |
|
|
59
|
+
| ----------------- | --------------------------------------------------------- |
|
|
60
|
+
| `Literal{value}` | A value hashed inline. |
|
|
61
|
+
| `Ref{from, path}` | The result of node `from`, projected along `path`. |
|
|
62
|
+
| `Pending{from}` | An ordering reference to node `from`, consuming no value. |
|
|
63
|
+
|
|
64
|
+
The tag is hashed, so `Pending{from}` and `Ref{from, path: []}` cannot collide
|
|
65
|
+
even though both resolve to the same dependency digest.
|
|
66
|
+
|
|
67
|
+
`KeyMaterial.dependencies` reads the graph-local dependencies a material names,
|
|
68
|
+
in declaration order and without duplicates. It is the single derivation of a
|
|
69
|
+
node's edge set, which is why a hashed reference and an edge can never disagree.
|
|
70
|
+
|
|
71
|
+
## Sealed, and everything else
|
|
72
|
+
|
|
73
|
+
Only `sealed` material may become a content key. `StepKey.fromKeyMaterial` fails
|
|
74
|
+
`non_content_material` for the other two tiers, and so does
|
|
75
|
+
`StepKey.dispatchIdentity`.
|
|
76
|
+
|
|
77
|
+
`StepKey.ordinal` mints the deliberately run-local key of compensable,
|
|
78
|
+
irreversible, or unsealed work, from a run id, an optional parent scope, an
|
|
79
|
+
ordinal, and a `tier` of `compensable`, `irreversible`, or `unsealed`. Those
|
|
80
|
+
keys cannot be reused across runs, which is the point: work that changed the
|
|
81
|
+
world outside the workspace must not be served from a cache.
|
|
82
|
+
|
|
83
|
+
## What a plan key folds in
|
|
84
|
+
|
|
85
|
+
`planIdentity` substitutes each `Ref` and `Pending` for the already computed
|
|
86
|
+
key of the referenced node. For sealed material it delegates to
|
|
87
|
+
`fromKeyMaterial`; other tiers produce a tier-bearing declaration fingerprint.
|
|
88
|
+
|
|
89
|
+
Structural node ids do not enter the plan key. Renaming a node preserves its
|
|
90
|
+
declaration fingerprint. A non-cacheable execution key does include its
|
|
91
|
+
structural address: renaming an effect must not accidentally replay a different
|
|
92
|
+
invocation. Changing what a node consumes changes downstream plan keys.
|
|
93
|
+
|
|
94
|
+
A dependency digest is resolved as an own data property. A `Ref` naming
|
|
95
|
+
`toString` or `constructor` is a `missing_dependency` refusal rather than a
|
|
96
|
+
colliding key, and so is a digest supplied through an accessor or as a non-string.
|
|
97
|
+
|
|
98
|
+
## What a dispatch key folds in instead
|
|
99
|
+
|
|
100
|
+
A plan key folds the resolved keys of every upstream node, transitively, so an
|
|
101
|
+
edit anywhere upstream re-keys everything below it, even when the edited node's
|
|
102
|
+
output value is byte for byte what it was before.
|
|
103
|
+
|
|
104
|
+
`dispatchIdentity` gives the JSON value channel the property the file channel
|
|
105
|
+
already had through measured boundary digests. It folds the node's own material
|
|
106
|
+
and never an upstream key. Each input contributes content instead: a `Literal`
|
|
107
|
+
its value, a `Ref` the digest of the settled result of `from` projected along
|
|
108
|
+
`path`, and a `Pending` nothing beyond its tag, because ordering does not change
|
|
109
|
+
what a node consumes.
|
|
110
|
+
|
|
111
|
+
`results` must hold every dependency the material names. The scheduler's halt
|
|
112
|
+
rule guarantees it: a dependent of failed or skipped work never dispatches, so a
|
|
113
|
+
`Ref` always resolves against a success.
|
|
114
|
+
|
|
115
|
+
## Projection
|
|
116
|
+
|
|
117
|
+
`StepKey.project` is the one projection semantics for the value channel. It
|
|
118
|
+
resolves only own data properties, so a path segment that is missing, inherited,
|
|
119
|
+
or an accessor yields `undefined` and no getter runs during key derivation.
|
|
120
|
+
|
|
121
|
+
Walking off the end of a result is a fact about the graph, not a failure.
|
|
122
|
+
`undefined` drops out of the canonical form, so it hashes distinctly from every
|
|
123
|
+
JSON value including `null`.
|
|
124
|
+
|
|
125
|
+
Every consumer that resolves a `Ref` at execution time must resolve it this same
|
|
126
|
+
way. Two inputs that key identically but are consumed differently is a stale-hit
|
|
127
|
+
vector, and one shared projection is what closes it.
|
|
128
|
+
|
|
129
|
+
## The collisions this compiler refuses
|
|
130
|
+
|
|
131
|
+
Each of these is a fix that stayed.
|
|
132
|
+
|
|
133
|
+
**A literal that looks like a digest.** The brand behind `StepKey.digestInput`
|
|
134
|
+
is private, so a plain object that merely has a `digest` field hashes as a
|
|
135
|
+
literal. Actions pass content hashes around as ordinary data; shape sniffing
|
|
136
|
+
would have hashed a genuine upstream-result reference and an ordinary content
|
|
137
|
+
hash identically.
|
|
138
|
+
|
|
139
|
+
**Environment material merged into the caller's.** `environment` is hashed in
|
|
140
|
+
its own namespace, so `caller{fs:["a"]} + env{fs:["b"]}` cannot alias
|
|
141
|
+
`caller{fs:["a","b"]} + env{}`. Environment layers keep declaration order,
|
|
142
|
+
because composition order can change behavior; caller-owned layers are
|
|
143
|
+
set-normalized.
|
|
144
|
+
|
|
145
|
+
**An unknown environment serving a cross-run hit.** `EnvironmentIdentity` is a
|
|
146
|
+
discriminated union. A declared environment carries no `runScope`; an undeclared
|
|
147
|
+
one must carry a non-empty one, which pins the key to a single run. Both
|
|
148
|
+
`content` and `dispatchIdentity` enforce that at run time with
|
|
149
|
+
`invalid_environment`.
|
|
150
|
+
|
|
151
|
+
**A bumped format keying like the old one.** `version` is folded into every
|
|
152
|
+
hashed body, so a change to what material means re-keys every node derived from
|
|
153
|
+
it.
|
|
154
|
+
|
|
155
|
+
## Sharing work between keys
|
|
156
|
+
|
|
157
|
+
`StepKey.makeDigestMemo` creates a memo that shares one in-flight projected
|
|
158
|
+
value digest between concurrent callers, addressed by the `[from, path]` tuple.
|
|
159
|
+
Pass it to `dispatchIdentity` through `digestMemo` when several keys in one
|
|
160
|
+
wavefront project the same upstream result.
|
|
161
|
+
|
|
162
|
+
Entries are sound only while each settled `from` value is immutable, so create a
|
|
163
|
+
fresh memo when those values can change.
|
|
164
|
+
|
|
165
|
+
A waiter never inherits the leader's interruption. If the leader's fiber is
|
|
166
|
+
cancelled, the waiter competes to become the replacement leader and recomputes,
|
|
167
|
+
rather than failing on an interrupt it did not request.
|
|
168
|
+
|
|
169
|
+
## Failures
|
|
170
|
+
|
|
171
|
+
`StepKey.KeyMaterialError` carries one of three codes:
|
|
172
|
+
|
|
173
|
+
| `code` | Cause |
|
|
174
|
+
| ---------------------- | ----------------------------------------------------------------------------- |
|
|
175
|
+
| `invalid_environment` | An environment identity that is neither properly declared nor run-scoped. |
|
|
176
|
+
| `missing_dependency` | A reference whose digest or settled result is absent, inherited, or mistyped. |
|
|
177
|
+
| `non_content_material` | A content or dispatch key was asked for on non-sealed material. |
|
|
178
|
+
|
|
179
|
+
[Troubleshooting](../troubleshooting.md) states the fix for each one.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Author a node graph"
|
|
3
|
+
description: "Build topology with the Node combinators, pass step results as planned values, and turn the result into the drafts Plan.compile takes."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`Node` is the authoring AST: pure, pipeable values that describe what a run will
|
|
9
|
+
do. This guide builds a graph with it and turns that graph into
|
|
10
|
+
`Plan.NodeDraft` values.
|
|
11
|
+
|
|
12
|
+
Most flow authors reach this layer through [`@smthrs/flow`](/api/flow), which
|
|
13
|
+
walks a flow body for you. Reach for `Node` directly when you are writing that
|
|
14
|
+
walk, testing topology, or composing a graph outside a flow declaration.
|
|
15
|
+
|
|
16
|
+
## Start from a value
|
|
17
|
+
|
|
18
|
+
`Node.succeed` lifts a constant. Nothing runs:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import * as Node from "@smthrs/plan/Node"
|
|
22
|
+
|
|
23
|
+
const lint = Node.succeed("lint")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Transform with map
|
|
27
|
+
|
|
28
|
+
`Node.map` defers a pure function. It is digested at build time and applied to
|
|
29
|
+
the real value later:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const shouted = Node.map(Node.succeed({ name: "world" }), (value) => value.name.toUpperCase())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `map` for computation only. A `map` that chooses what happens next is a
|
|
36
|
+
`branch` written wrongly, and the plan loses the topology a reviewer needs to
|
|
37
|
+
see.
|
|
38
|
+
|
|
39
|
+
## Sequence nodes and bind planned references
|
|
40
|
+
|
|
41
|
+
`Node.andThen(first, next)` sequences two nodes when the first result is not needed.
|
|
42
|
+
Use `Node.bindPlanned(first, build)` when the next node consumes a reference to
|
|
43
|
+
that result. The builder runs once at plan time against a `Planned` placeholder:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Action } from "@smthrs/flow"
|
|
47
|
+
import type * as Planned from "@smthrs/plan/Planned"
|
|
48
|
+
import * as Schema from "effect/Schema"
|
|
49
|
+
|
|
50
|
+
const Draft = Action.make("docs/Draft", {
|
|
51
|
+
payload: { topic: Schema.String },
|
|
52
|
+
success: Schema.String,
|
|
53
|
+
error: Schema.String
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const Publish = Action.make("docs/Publish", {
|
|
57
|
+
payload: { text: Schema.String, urgent: Schema.Boolean },
|
|
58
|
+
success: Schema.String
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const article = Node.bindPlanned(
|
|
62
|
+
Draft.call({ topic: "durable plans" }),
|
|
63
|
+
(text: Planned.Planned<string>) => Publish.call({ text, urgent: false })
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`text` is a placeholder, not a string. Pass it into a payload field, read a
|
|
68
|
+
field off it, hand it to a branch: all of that records a reference. Computing on
|
|
69
|
+
it can throw `planned_value_computed` with the node and path named. JavaScript
|
|
70
|
+
truthiness and strict equality cannot be trapped: they inspect the reference,
|
|
71
|
+
not its future result. Use `Node.branch` for decisions and `Node.map` for real-value
|
|
72
|
+
computation. Enable the type-aware ESLint rule
|
|
73
|
+
`@typescript-eslint/strict-boolean-expressions: "error"` to reject planned values
|
|
74
|
+
in conditions. Explicit `Boolean(reference)` and reference equality still require
|
|
75
|
+
review. [The authoring AST](../concepts/authoring-ast.md) explains the reference contract.
|
|
76
|
+
|
|
77
|
+
## Decide with branch
|
|
78
|
+
|
|
79
|
+
Both arms are built once, symbolically, so the plan carries the exit condition
|
|
80
|
+
and both continuations before anything runs:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const decided = Node.bindPlanned(
|
|
84
|
+
Draft.call({ topic: "durable plans" }),
|
|
85
|
+
(text: Planned.Planned<string>) =>
|
|
86
|
+
Node.branch(Node.succeed({ urgent: true }), {
|
|
87
|
+
if: (flags) => flags.urgent,
|
|
88
|
+
then: () => Publish.call({ text, urgent: true }),
|
|
89
|
+
else: () => Publish.call({ text, urgent: false })
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`if` runs later, on the real value. `then` and `else` run now, once each.
|
|
95
|
+
|
|
96
|
+
## Recover with catch
|
|
97
|
+
|
|
98
|
+
`Node.catch` stores the failure arm as topology beside the protected graph.
|
|
99
|
+
Without an `error` schema it handles the whole typed error channel; with one it
|
|
100
|
+
handles only the values that schema accepts, and the rest survives in the
|
|
101
|
+
resulting error type:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const recovered = Node.catch(Draft.call({ topic: "fallbacks" }), {
|
|
105
|
+
onFailure: () => Node.succeed("draft unavailable")
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Fan out with all
|
|
110
|
+
|
|
111
|
+
`Node.all` combines independent children by name. Its width is fixed here, at
|
|
112
|
+
build time:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const batched = Node.all({
|
|
116
|
+
lint: Node.priority(Node.succeed("lint"), 10),
|
|
117
|
+
types: Node.succeed("types")
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
To fan out over something a step discovered, end the round and carry the list in
|
|
122
|
+
the next flow's payload, where it is real data. A plan is a DAG and cannot grow
|
|
123
|
+
a leg it did not declare.
|
|
124
|
+
|
|
125
|
+
## Prioritize without re-keying
|
|
126
|
+
|
|
127
|
+
`Node.priority` attaches a scheduling priority and leaves the original node
|
|
128
|
+
unchanged. Higher runs first among ready work, so a priority changes latency and
|
|
129
|
+
nothing else. It never enters key material, so a prioritized node keeps its
|
|
130
|
+
cache hit. Children inherit the value lexically when the graph is built, and a
|
|
131
|
+
child that states its own keeps it.
|
|
132
|
+
|
|
133
|
+
`Node.declaredPriority(ast)` reads back the value a node states, or `undefined`
|
|
134
|
+
when it inherits.
|
|
135
|
+
|
|
136
|
+
`Node.priority` refuses anything that is not a safe integer with
|
|
137
|
+
`invalid_priority`, because no ordering could compare it.
|
|
138
|
+
|
|
139
|
+
## Declare captures for a deterministic digest
|
|
140
|
+
|
|
141
|
+
The AST stores a digest of a function's exact source, never the closure. A
|
|
142
|
+
function with undeclared captures also carries process-local entropy, so two
|
|
143
|
+
indistinguishable sources fail closed rather than sharing a cache key. Declare
|
|
144
|
+
the inert values a function closes over to get a deterministic identity instead:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const suffixed = Node.capture({ suffix: "!" }, (value: string) => `${value}!`)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Turn the graph into drafts
|
|
151
|
+
|
|
152
|
+
`Plan.compile` takes `NodeDraft` values, and [`@smthrs/flow`](/api/flow) is what
|
|
153
|
+
produces them. `Graph.build` walks a flow declaration or a bare node once;
|
|
154
|
+
`Graph.drafts` reads the drafts back:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { Flow, Graph } from "@smthrs/flow"
|
|
158
|
+
import * as Plan from "@smthrs/plan/Plan"
|
|
159
|
+
|
|
160
|
+
const Article = Flow.make("docs/Article", {
|
|
161
|
+
payload: { topic: Schema.String },
|
|
162
|
+
success: Schema.String,
|
|
163
|
+
error: Schema.String,
|
|
164
|
+
body: ({ topic }: { readonly topic: string }) =>
|
|
165
|
+
Node.bindPlanned(Draft.call({ topic }), (text: Planned.Planned<string>) => Publish.call({ text, urgent: false }))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const drafts: ReadonlyArray<Plan.NodeDraft> = Graph.drafts(Graph.build(Article, { topic: "plans" }))
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`Graph.drafts` throws the first typed build refusal rather than returning
|
|
172
|
+
partial drafts, because returning them would turn missing topology into a valid
|
|
173
|
+
plan. `Graph.diagnostics` lists the recoverable issues a build recorded; fatal
|
|
174
|
+
refusals, including computing on a planned value, throw from `Graph.build`
|
|
175
|
+
itself.
|
|
176
|
+
|
|
177
|
+
## Next
|
|
178
|
+
|
|
179
|
+
- [Compile drafts into a plan](./compile-a-plan.md): keys, ordering, conflict
|
|
180
|
+
annotation, and the digest.
|
|
181
|
+
- [The authoring AST](../concepts/authoring-ast.md): why building runs nothing,
|
|
182
|
+
and how a closure survives as a digest.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Compile drafts into a plan"
|
|
3
|
+
description: "Turn NodeDraft values into a keyed graph: what compile does in order, the one service it needs, and every refusal it can answer with."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`Plan.compile` is the whole plan phase in one function. Hand it drafts, get back
|
|
9
|
+
a keyed graph with a digest.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
|
|
13
|
+
import * as KeyMaterial from "@smthrs/plan/KeyMaterial"
|
|
14
|
+
import * as Plan from "@smthrs/plan/Plan"
|
|
15
|
+
import * as Effect from "effect/Effect"
|
|
16
|
+
|
|
17
|
+
const readPr: Plan.NodeDraft = {
|
|
18
|
+
id: "read-pr",
|
|
19
|
+
material: {
|
|
20
|
+
version: KeyMaterial.version,
|
|
21
|
+
kind: "sealed",
|
|
22
|
+
body: { action: "read-pr", pr: 4821 },
|
|
23
|
+
inputs: [],
|
|
24
|
+
layers: [],
|
|
25
|
+
capabilities: ["net:get"]
|
|
26
|
+
},
|
|
27
|
+
effects: { reads: [], writes: ["pr.json"], boundaryMode: "hard" }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const runTests: Plan.NodeDraft = {
|
|
31
|
+
id: "run-tests",
|
|
32
|
+
material: {
|
|
33
|
+
version: KeyMaterial.version,
|
|
34
|
+
kind: "sealed",
|
|
35
|
+
body: { action: "run-tests" },
|
|
36
|
+
inputs: [{ _tag: "Ref", from: "read-pr", path: [] }],
|
|
37
|
+
layers: [],
|
|
38
|
+
capabilities: []
|
|
39
|
+
},
|
|
40
|
+
effects: { reads: ["pr.json"], writes: ["report.json"], boundaryMode: "hard" },
|
|
41
|
+
priority: 10,
|
|
42
|
+
conflictStrategy: "serialize",
|
|
43
|
+
runtimeStrategy: "delay-rebase"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const compiled = Plan.compile({
|
|
47
|
+
planId: "review-4821",
|
|
48
|
+
flow: "example/Review",
|
|
49
|
+
nodes: [readPr, runTests]
|
|
50
|
+
}).pipe(Effect.provide(NodeCrypto.layer))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## What a draft carries
|
|
54
|
+
|
|
55
|
+
| Field | Required | What it decides |
|
|
56
|
+
| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| `id` | yes | The durable lookup address. Not part of the declaration fingerprint; non-cacheable execution uses it as a run-local scope. |
|
|
58
|
+
| `material` | yes | Everything the key is derived from. |
|
|
59
|
+
| `effects` | yes | Declared reads, writes, optional removals, and the boundary mode. |
|
|
60
|
+
| `kind` | no | `step`, `agent`, or `merge`. Defaults to `step`. |
|
|
61
|
+
| `priority` | no | Scheduling tie-break. Defaults to 0. |
|
|
62
|
+
| `conflictStrategy` | no | This node's preferred plan-time verdict for an overlap. Defaults to `serialize`. |
|
|
63
|
+
| `runtimeStrategy` | no | This node's preferred response when an overlap bites. Defaults to `delay-rebase`. |
|
|
64
|
+
|
|
65
|
+
## What compile does, in order
|
|
66
|
+
|
|
67
|
+
1. Validates `planId`, `flow`, and every draft, decoding `material` and
|
|
68
|
+
`effects` through their schemas.
|
|
69
|
+
2. Puts the drafts in topological order from the dependencies their material
|
|
70
|
+
names.
|
|
71
|
+
3. Keys each node, substituting every `Ref` and `Pending` for the already
|
|
72
|
+
computed key of the node it names.
|
|
73
|
+
4. Annotates overlapping write sets and adds the ordering edges the verdicts
|
|
74
|
+
imply.
|
|
75
|
+
5. Adds reader-after-writer edges where explicit `Ref`/`Pending` paths have not
|
|
76
|
+
already selected read-before-write sequencing. A read before a writer
|
|
77
|
+
consumes an earlier version, not that later writer's output. Contradictory
|
|
78
|
+
inferred orderings still fail with `cycle`.
|
|
79
|
+
6. Derives the plan digest over the whole result.
|
|
80
|
+
|
|
81
|
+
The nodes come back in topological order of material dependencies, deep-frozen,
|
|
82
|
+
at generation 0, with `baseDigest` equal to `digest`. Inferred edges extend
|
|
83
|
+
`dependsOn` without reordering the array. For example, a reader declared before
|
|
84
|
+
an independent writer stays first in `plan.nodes`, even when its `dependsOn`
|
|
85
|
+
contains that writer. Schedule nodes from the complete `dependsOn` graph;
|
|
86
|
+
iterating the array alone can run a reader before its producer.
|
|
87
|
+
|
|
88
|
+
## The one service it needs
|
|
89
|
+
|
|
90
|
+
Compiling asks for Effect's `Crypto` service and nothing else. Provide it from a
|
|
91
|
+
platform package:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
|
|
95
|
+
|
|
96
|
+
const plan = compiled.pipe(Effect.provide(NodeCrypto.layer))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
In a browser, provide the browser platform's `Crypto` layer instead. No part of
|
|
100
|
+
compilation reaches a file, a clock, or a network.
|
|
101
|
+
|
|
102
|
+
## Your drafts are copied, not held
|
|
103
|
+
|
|
104
|
+
`compile` snapshots its options before it does anything else, and the plan it
|
|
105
|
+
returns is deep-frozen. Mutating a draft afterwards cannot change the plan, its
|
|
106
|
+
keys, or its digest. Material is stored as the inert JSON mirror its key already
|
|
107
|
+
covers, so a `Date` or `URL` in a body is stored as the value it serializes to.
|
|
108
|
+
|
|
109
|
+
## When compile refuses
|
|
110
|
+
|
|
111
|
+
`PlanError` is a closed set of seven codes, so a caller can switch on `code`.
|
|
112
|
+
|
|
113
|
+
| `code` | Cause |
|
|
114
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `cycle` | Material dependencies close a cycle, or a reader-after-writer edge would close one. |
|
|
116
|
+
| `unknown_dependency` | A `Ref` or `Pending` names a node that is neither in the drafts nor already in the plan. |
|
|
117
|
+
| `duplicate_node` | A draft reuses an id the plan already holds. |
|
|
118
|
+
| `overlap_forbidden` | A `fail` pair genuinely overlaps and no dependency path orders it. |
|
|
119
|
+
| `invalid_effects` | One path is declared as both a write and a removal. |
|
|
120
|
+
| `invalid_node` | An empty id or flow, a priority that is not a safe integer, a value outside a literal set, or material this release cannot decode. |
|
|
121
|
+
| `graph_too_large` | The plan would hold more than `Plan.maximumPlanNodes` nodes. |
|
|
122
|
+
|
|
123
|
+
All three effect tiers compile. `StepKey.planIdentity` produces approval-bound
|
|
124
|
+
declaration fingerprints; compilation does not grant cache eligibility.
|
|
125
|
+
`compile` can also fail with `StepKey.KeyMaterialError` when a dependency
|
|
126
|
+
digest is missing, and with a `Schema.SchemaError` from decoding.
|
|
127
|
+
[Troubleshooting](../troubleshooting.md) states the fix for each one.
|
|
128
|
+
|
|
129
|
+
## Size
|
|
130
|
+
|
|
131
|
+
`Plan.maximumPlanNodes` is 10,000. The conflict and reader-after-writer passes
|
|
132
|
+
compare node pairs, so the cost is at least quadratic in node count, and each
|
|
133
|
+
pair whose write sets actually overlap adds one on-demand reachability walk.
|
|
134
|
+
A plan above the ceiling is refused before any pair is compared.
|
|
135
|
+
|
|
136
|
+
If you are approaching the ceiling, split the work across flow boundaries rather
|
|
137
|
+
than raising it: a plan that large is also a plan no operator can review.
|
|
138
|
+
|
|
139
|
+
## Next
|
|
140
|
+
|
|
141
|
+
- [Persist a plan](https://plan-store.smithers.sh/guides/persist-a-plan/): record the compiled value and read it
|
|
142
|
+
back.
|
|
143
|
+
- [Declare the files a node touches](./declare-file-effects.md): the vocabulary
|
|
144
|
+
behind step 4 and step 5.
|
|
145
|
+
- [Step keys](../concepts/step-keys.md): what step 3 folds in, and what it
|
|
146
|
+
deliberately leaves out.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Declare the files a node touches"
|
|
3
|
+
description: "Write a node's read, write, and removal sets with FileSet: literal paths, globs, tree artifacts, filegroups, and the forms a workspace-relative path may not take."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`FileSet` is the static filesystem vocabulary planning and execution share.
|
|
9
|
+
A node's `effects` are written in it, and everything the plan knows about
|
|
10
|
+
conflicts comes from what you declare here.
|
|
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
|
+
## The four declaration forms
|
|
24
|
+
|
|
25
|
+
| Form | Written as | Where it is valid |
|
|
26
|
+
| -------------- | ----------------------------------------- | -------------------------- |
|
|
27
|
+
| `Pattern` | A workspace-relative literal path string. | reads, writes, and removes |
|
|
28
|
+
| `Glob` | `{_tag: "Glob", include, exclude?}` | reads and writes |
|
|
29
|
+
| `TreeArtifact` | `{_tag: "TreeArtifact", path}` | writes only |
|
|
30
|
+
| `Filegroup` | `{_tag: "Filegroup", name, entries}` | reads and writes |
|
|
31
|
+
|
|
32
|
+
Bare string entries are literal paths at both overlap analysis and execution,
|
|
33
|
+
even when they contain `*` or `**`. Use `Glob.include` and `Glob.exclude` for
|
|
34
|
+
wildcard matching in read and write declarations.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import * as FileSet from "@smthrs/plan/FileSet"
|
|
38
|
+
|
|
39
|
+
const sources: FileSet.Glob = {
|
|
40
|
+
_tag: "Glob",
|
|
41
|
+
include: ["src/**/*.ts"],
|
|
42
|
+
exclude: ["src/**/*.test.ts"]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const bundle: FileSet.TreeArtifact = { _tag: "TreeArtifact", path: "dist" }
|
|
46
|
+
|
|
47
|
+
const group: FileSet.Filegroup = FileSet.makeFilegroup("sources", [sources, "package.json"])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A tree artifact is a whole directory captured and replayed as one output. It
|
|
51
|
+
belongs in a write set, and `ReadEntry` deliberately excludes it: a read set
|
|
52
|
+
names what the node consumes, and a directory output is not that.
|
|
53
|
+
|
|
54
|
+
## Expanding a filegroup
|
|
55
|
+
|
|
56
|
+
A filegroup is a name for a reusable collection. Both plan passes expand groups
|
|
57
|
+
before they compare anything, and `expand` does the same for a caller,
|
|
58
|
+
deterministically and in declaration order:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const flattened: ReadonlyArray<FileSet.Entry> = FileSet.expand([group, bundle])
|
|
62
|
+
const reads: ReadonlyArray<FileSet.ReadEntry> = FileSet.expandReads([
|
|
63
|
+
{ _tag: "Filegroup", name: "sources", entries: [sources] }
|
|
64
|
+
])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`expandReads` is the read-set counterpart, typed so a tree artifact cannot
|
|
68
|
+
appear in the result.
|
|
69
|
+
|
|
70
|
+
## What a workspace-relative path may not be
|
|
71
|
+
|
|
72
|
+
`workspaceRelative` decides whether a declaration names a file one way only. It
|
|
73
|
+
refuses:
|
|
74
|
+
|
|
75
|
+
- Absolute paths, POSIX (`/etc/passwd`) and drive-letter (`C:/tmp`), and the
|
|
76
|
+
drive-relative `C:tmp`.
|
|
77
|
+
- Upward traversal: any `..` segment.
|
|
78
|
+
- Aliasing forms: `.` segments and empty segments.
|
|
79
|
+
- C0 controls (U+0000 through U+001F) and DEL (U+007F).
|
|
80
|
+
|
|
81
|
+
C1 controls (U+0080 through U+009F) stay legal, because those bytes are valid in
|
|
82
|
+
a POSIX file name.
|
|
83
|
+
|
|
84
|
+
The aliasing rules are correctness, not tidiness. Two spellings of one file that
|
|
85
|
+
the overlap check cannot see would let a plan run a reader and its writer in the
|
|
86
|
+
same wavefront round.
|
|
87
|
+
|
|
88
|
+
## Canonical form
|
|
89
|
+
|
|
90
|
+
`canonical` rewrites every separator to `/` and normalizes Unicode to NFC. Every
|
|
91
|
+
exact-path comparison in the module goes through it:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const same = FileSet.canonical("dist\\bundle.js") === FileSet.canonical("dist/bundle.js") // true
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
So the backslash spelling and the NFD spelling of one workspace path overlap.
|
|
98
|
+
A glob, by contrast, tests the path bytes it is handed: canonicalizing a
|
|
99
|
+
_measured_ path before matching it is the caller's decision, because a backslash
|
|
100
|
+
inside a path segment is literal text on a POSIX filesystem.
|
|
101
|
+
|
|
102
|
+
Overlap goes one step further and compares `folded`, the canonical form with
|
|
103
|
+
letter case folded away. A case-insensitive filesystem opens `src/A.ts` and
|
|
104
|
+
`src/a.ts` as one file, so two nodes writing them serialize.
|
|
105
|
+
|
|
106
|
+
## Matching and overlap
|
|
107
|
+
|
|
108
|
+
| Function | Answers |
|
|
109
|
+
| ------------------------------- | ------------------------------------------------- |
|
|
110
|
+
| `matchesPattern(pattern, path)` | Whether one path matches one Bazel-style pattern. |
|
|
111
|
+
| `matchesGlob(glob, path)` | The same, honoring the glob's `exclude` list. |
|
|
112
|
+
| `overlaps(left, right)` | Whether two entries might share a path. |
|
|
113
|
+
| `isGlob`, `isTreeArtifact` | Which variant an entry is. |
|
|
114
|
+
|
|
115
|
+
In `Glob.include`, `Glob.exclude`, and the `matchesPattern` helper, `**` matches
|
|
116
|
+
path segments; `*` matches within one segment and never crosses a separator. A
|
|
117
|
+
trailing `**` matches the rest of the path. Calling `matchesPattern` directly does
|
|
118
|
+
not change how a bare string declaration is interpreted.
|
|
119
|
+
|
|
120
|
+
`overlaps` is conservative by design: `true` may over-serialize, and `false`
|
|
121
|
+
proves that no path can belong to both declarations. Two globs always overlap,
|
|
122
|
+
and so do a glob and a tree artifact.
|
|
123
|
+
|
|
124
|
+
## Writes and removals are one set
|
|
125
|
+
|
|
126
|
+
A removal mutates the world exactly as a write does, so both plan passes fold
|
|
127
|
+
`writes` and `removes` together. Declaring one path as both fails compilation
|
|
128
|
+
with `invalid_effects`, because the declaration contradicts itself.
|
|
129
|
+
|
|
130
|
+
`removes` accepts literal path strings only, not globs or tree artifacts.
|
|
131
|
+
|
|
132
|
+
## Boundary mode
|
|
133
|
+
|
|
134
|
+
`boundaryMode` is `hard` or `expected`. It travels with the declaration into the
|
|
135
|
+
measured boundary at execution time. The plan passes do not interpret it: they
|
|
136
|
+
only need to know which paths a node produces and which it consumes.
|
|
137
|
+
|
|
138
|
+
## Next
|
|
139
|
+
|
|
140
|
+
- [Declared effects and conflicts](../concepts/effects-and-conflicts.md): what
|
|
141
|
+
the two plan passes do with these declarations.
|
|
142
|
+
- [Compile drafts into a plan](./compile-a-plan.md): where effects are decoded
|
|
143
|
+
and written back into key material.
|