@smthrs/plan 0.0.0-stage → 1.0.0-rc.4
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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# @smthrs/plan
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Keep live executable bindings and planned-reference ownership across compatible package copies; serialized ASTs remain inert. ([#1991](https://github.com/smithersai/smithers/issues/1991)).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `Scheduling.make` exposes the pure ready-work priority, aging and capacity
|
|
14
|
+
policy used by the durable plan scheduler. Its exact integer comparison does
|
|
15
|
+
not round away priority differences after aging. Conditional readiness and
|
|
16
|
+
effect execution remain the coordinator's responsibilities.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `StepKey.EnvironmentIdentity` now also supplies the runtime schema. Identity
|
|
21
|
+
constructors reject malformed/unknown environment fields with typed errors.
|
|
22
|
+
`StepKey.environmentIdentity` fingerprints a runtime environment for durable
|
|
23
|
+
recovery binding, using content-key normalization in a separate namespace.
|
|
24
|
+
Existing valid content and dispatch keys are unchanged.
|
|
25
|
+
|
|
26
|
+
- Explicit `Ref`/`Pending` read-before-write sequences compile and verify,
|
|
27
|
+
including transitive paths and appended generations. Conflicting inferred
|
|
28
|
+
producer/serialize orders still fail with `cycle`.
|
|
29
|
+
|
|
30
|
+
- `Plan.compile`, `append`, and `verify` accept compensable and irreversible
|
|
31
|
+
declarations. New `StepKey.planIdentity` separates their approval fingerprints
|
|
32
|
+
from cache eligibility. Existing sealed keys are unchanged;
|
|
33
|
+
`fromKeyMaterial` and `dispatchIdentity` remain sealed-only.
|
|
34
|
+
|
|
35
|
+
## [1.0.0-rc.0] - 2026-08-31
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- The persisted plan: `Plan` (compile, append, conflict annotation),
|
|
40
|
+
`PlanStore` (append-only SQL, migration block `4000`), `PlanDiff`, and the
|
|
41
|
+
`KeyMaterial` to `StepKey` compiler revived from the deleted
|
|
42
|
+
`packages/smithers/flows/keys` modules, now producing `@smthrs/keys` `Key`
|
|
43
|
+
values rather than a second digest format.
|
|
44
|
+
- Added `Node.priority`, which attaches a scheduling priority to a node, and
|
|
45
|
+
`Node.declaredPriority`, which reads one back. The value is a plain JSON
|
|
46
|
+
field on the AST rather than a `Context` annotation, so a stored plan keeps
|
|
47
|
+
it. `Plan.compile` copies it from `NodeDraft.priority` onto the plan node and
|
|
48
|
+
folds it into the plan digest a human approves; it never enters a node's key
|
|
49
|
+
material, so raising a priority reorders work without re-keying a step. A
|
|
50
|
+
priority that is not a safe integer is a `GraphBuildError` with the new
|
|
51
|
+
`invalid_priority` code.
|
|
52
|
+
- Added package-owned documentation. `packages/plan/docs` and the public JSDoc
|
|
53
|
+
in `src` are the single source for the published API page.
|
|
54
|
+
|
|
55
|
+
### Breaking Changes
|
|
56
|
+
|
|
57
|
+
- Bumped key material to `flows/key-material/v2` and captured-function
|
|
58
|
+
identity to `sha256-source-captures/v4`. Existing persisted step keys are
|
|
59
|
+
intentionally invalidated rather than being reused across changed identity
|
|
60
|
+
semantics.
|
|
61
|
+
|
|
62
|
+
- Plan approval digests now include each node's conflict and runtime
|
|
63
|
+
strategies. This moves plan digests without moving step keys. A running run
|
|
64
|
+
pinned to the old digest needs a new approval before it can use the changed
|
|
65
|
+
plan.
|
|
66
|
+
- A node payload is now cloned as its JSON mirror, so a payload carrying a
|
|
67
|
+
`toJSON`, such as a `Date` or a `URL`, keys as the value it serializes to
|
|
68
|
+
rather than as an empty object. Payloads of that shape re-key once.
|
|
69
|
+
- A flow call's `KeyMaterial.body` no longer has a body-less shape. `Flow.make`
|
|
70
|
+
requires `body`, so `@smthrs/flow` never records `declaration.body:
|
|
71
|
+
undefined` for a call whose declaration survived beside its AST.
|
|
72
|
+
`StepKey.content` therefore folds a body digest into every such node, and the
|
|
73
|
+
keys `Plan.compile` derives for the flow calls that previously carried none
|
|
74
|
+
move with it.
|
|
75
|
+
- The raw migration steps moved beneath `src/internal/migrations`, so
|
|
76
|
+
`@smthrs/plan/migrations/0001_initial` and its siblings no longer resolve.
|
|
77
|
+
`Migrations.set` is the supported surface; a step imported on its own ran
|
|
78
|
+
outside the namespaced ordering `Migrator` relies on.
|
|
79
|
+
- `Node.isNode` no longer accepts the public `TypeId` marker on its own. It
|
|
80
|
+
recognizes a node this package built by registration at construction, and a
|
|
81
|
+
rehydrated node, an object sharing the node prototype whose own `ast` is a
|
|
82
|
+
well-formed AST, by that shape, which is what `@smthrs/flow` hands back for
|
|
83
|
+
an AST that crossed a serialization boundary. `Node.all`, `andThen`,
|
|
84
|
+
`branch`, and `catch` therefore refuse a forged object carrying the marker,
|
|
85
|
+
one inheriting it from a node, and one whose `ast` is missing, malformed, or
|
|
86
|
+
cyclic with their usual `GraphBuildError` instead of storing an `ast` the
|
|
87
|
+
package never built.
|
|
88
|
+
- `StepKey.EnvironmentIdentity` is now a discriminated union. A declared
|
|
89
|
+
environment may not carry a `runScope` and an undeclared one must, which
|
|
90
|
+
`StepKey.dispatchIdentity` also enforces at run time with the new
|
|
91
|
+
`invalid_environment` code.
|
|
92
|
+
- A plan may no longer contain an unbounded number of nodes. `Plan.compile` and
|
|
93
|
+
`Plan.append` refuse a plan above `Plan.maximumPlanNodes` with the new
|
|
94
|
+
`graph_too_large` code, before any pair comparison runs. The conflict pass
|
|
95
|
+
compares node pairs and adds a reachability walk per overlapping pair, so an
|
|
96
|
+
unbounded plan had an unbounded planning cost; a graph larger than the bound
|
|
97
|
+
now has to be split.
|
|
98
|
+
- `NodeDraft.effects` is now the single derivation point for effect identity.
|
|
99
|
+
`Plan.compile` and `Plan.append` fold the decoded declaration into the hashed
|
|
100
|
+
material, replacing any `material.effects` a caller supplied. Every plan
|
|
101
|
+
re-keys once, and editing `reads`, `writes`, `removes`, or `boundaryMode`
|
|
102
|
+
now re-keys the node and is attributed by `PlanDiff` as `effects`. Before
|
|
103
|
+
this, the two channels were independent: a draft could change what a node
|
|
104
|
+
writes without moving its key, and `PlanDiff` reported it `unchanged`.
|
|
105
|
+
- Compiled material is stored as its deeply frozen JSON mirror. A `Date`, a
|
|
106
|
+
`URL`, or any value with a data-valued callable `toJSON` is stored as the
|
|
107
|
+
value it serializes to, which is what its key already covered. A material
|
|
108
|
+
accessor or a prototype with no JSON representation is now refused with
|
|
109
|
+
`invalid_node` rather than passed through by reference; the message names the
|
|
110
|
+
node and the payload path and never the value.
|
|
111
|
+
- A payload whose `toJSON` returns its own receiver now fails with the
|
|
112
|
+
`cyclic_payload` `GraphBuildError` instead of being silently omitted from the
|
|
113
|
+
clone. The clone and the input now agree: canonical serialization refused
|
|
114
|
+
that shape all along, while the clone quietly keyed as `{}`.
|
|
115
|
+
|
|
116
|
+
### Fixed
|
|
117
|
+
|
|
118
|
+
- The migration steps under `internal/migrations` are named exports, and
|
|
119
|
+
`Migrations` imports them by name. The CommonJS build compiled each
|
|
120
|
+
`export default` step to `exports.default`, and Node's default-import interop
|
|
121
|
+
then handed the importer the whole exports object instead of the Effect, so
|
|
122
|
+
`require("@smthrs/plan/Migrations")` produced a set whose entries had no
|
|
123
|
+
`pipe`.
|
|
124
|
+
- Plan payload capture now refuses accessors and unsupported prototypes with
|
|
125
|
+
`invalid_payload` instead of executing getters or collapsing `Map`, `Set`,
|
|
126
|
+
`RegExp`, and class instances onto the same `{}` identity.
|
|
127
|
+
- `StepKey.content` and `dispatchIdentity` now share one environment validator;
|
|
128
|
+
undeclared environments require a non-empty run scope on both paths.
|
|
129
|
+
|
|
130
|
+
- `PlanStore.append` now advances the stored plan row with a compare-and-swap
|
|
131
|
+
on the previous generation, so appending a skipped generation is refused
|
|
132
|
+
instead of persisting node rows whose dependencies are missing and which the
|
|
133
|
+
append-only triggers then forbid removing. Node ordinals are derived from the
|
|
134
|
+
store rather than from the caller's array, `PlanStore.record` refuses
|
|
135
|
+
anything that is not generation 0, and an append with no new nodes is
|
|
136
|
+
refused rather than advancing the generation for nothing.
|
|
137
|
+
- A node payload no longer keeps live functions, and different `Date`, `URL`
|
|
138
|
+
or other `toJSON` payloads no longer share one step key.
|
|
139
|
+
- `StepKey.fromKeyMaterial` resolves dependency digests as own properties, so a
|
|
140
|
+
dependency named `toString` or `constructor` is a `missing_dependency`
|
|
141
|
+
refusal rather than a colliding key.
|
|
142
|
+
- A `StepKey.DigestMemo` waiter no longer inherits the leader's interruption;
|
|
143
|
+
it recomputes its own digest instead.
|
|
144
|
+
- `StepKey.project` resolves only own data properties, so a projection path
|
|
145
|
+
through `toString`, `constructor` or `__proto__` yields `undefined` as
|
|
146
|
+
documented and no getter runs during key derivation.
|
|
147
|
+
- The conflict pass now decides orderedness against the live edge set, so a
|
|
148
|
+
writer pair a dependency path already orders is neither annotated, nor given
|
|
149
|
+
a redundant edge, nor refused with `overlap_forbidden`.
|
|
150
|
+
- `Plan.compile` now fails with `cycle` when a reader-after-writer edge would
|
|
151
|
+
close a cycle, because a declared dependency or a `serialize` edge already
|
|
152
|
+
orders the producer behind its reader. The edge was dropped before, which
|
|
153
|
+
left the reader ahead of its producer to measure and cache pre-producer
|
|
154
|
+
bytes as a legitimate execution. The message names the reader, the producer,
|
|
155
|
+
the overlapping paths, and the dependency chain.
|
|
156
|
+
- `Plan.compile` no longer recurses per dependency edge and no longer
|
|
157
|
+
materializes a transitive closure, so a large graph compiles in bounded
|
|
158
|
+
memory instead of raising an untyped `RangeError`.
|
|
159
|
+
- `Plan.compile` and `Plan.append` validate drafts before keying, with the new
|
|
160
|
+
`invalid_node` code, so an empty plan id, an unsafe-integer priority, or a
|
|
161
|
+
key material version this release does not know is refused at the plan rather
|
|
162
|
+
than at the store.
|
|
163
|
+
- `PlanDiff` attributes a key move caused by `nondeterministic` or `placement`,
|
|
164
|
+
and `Node.capture` refuses a capture nested past its documented limit with a
|
|
165
|
+
path-bearing error rather than overflowing the stack.
|
|
166
|
+
- `Plan.compile` and `Plan.append` decode `NodeDraft.effects` through
|
|
167
|
+
`NodeEffects` before keying, so a malformed declaration is a typed
|
|
168
|
+
`invalid_node` naming the node and the failing path instead of an untyped
|
|
169
|
+
defect. A filesystem declaration containing C0 controls or DEL, or one that is
|
|
170
|
+
not workspace-relative, is refused there rather than reaching a host
|
|
171
|
+
filesystem, and path comparison is Unicode NFC normalized, so two spellings of
|
|
172
|
+
one name are detected as an overlap. `invalid_effects` is now reserved for the
|
|
173
|
+
cross-field rule that one path cannot be both a write and a removal.
|
|
174
|
+
- `Planned.reference` validates the reference shape, so a forged carrier can no
|
|
175
|
+
longer inject a fabricated projection path into a payload.
|
|
176
|
+
- Compiled and appended plans now keep the exact material and filesystem
|
|
177
|
+
authority that their keys and approval digest cover, even if caller-owned
|
|
178
|
+
draft objects are later mutated.
|
|
179
|
+
- Persisted plans can no longer be deleted or have their plan id, flow, or
|
|
180
|
+
creation time rewritten, and node order is now unique and deterministic within
|
|
181
|
+
each plan. Migration `4003` adds the plan-id pin the forward-only trigger was
|
|
182
|
+
missing, which a rename could otherwise use to strand a plan's immortal node
|
|
183
|
+
rows under an id nothing can record again.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Cory and the Smithers Flows contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,229 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @smthrs/plan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Release candidate scope, host requirements and compatibility review are defined in the [library support policy](https://github.com/smithersai/smithers/blob/main/RELEASE_SUPPORT.md).
|
|
4
|
+
|
|
5
|
+
This package declares `effect` as an exact
|
|
6
|
+
`4.0.0-rc.115` peer dependency. Keep the application on that version so
|
|
7
|
+
all Smithers packages share one Effect runtime.
|
|
8
|
+
|
|
9
|
+
**Documentation:** https://plan.smithers.sh
|
|
10
|
+
|
|
11
|
+
The persisted plan: a keyed action graph, its append-only store, its diff, and
|
|
12
|
+
the step-key compiler that gives every node its identity.
|
|
13
|
+
|
|
14
|
+
`@smthrs/plan` turns a description of work into one durable value. It compiles
|
|
15
|
+
declared steps into a graph in which every node carries a content key derived
|
|
16
|
+
from what that node declares, records that graph in a SQLite store that only
|
|
17
|
+
ever grows, and compares two graphs to say exactly what changed. It runs
|
|
18
|
+
nothing itself: it performs no I/O beyond the database, and driving a compiled
|
|
19
|
+
plan is a scheduler's job, such as
|
|
20
|
+
[`@smthrs/engine-store`](https://engine-store.smithers.sh)'s `PlanScheduler`.
|
|
21
|
+
|
|
22
|
+
Reach for it when you are building a scheduler, a cache, or an approval flow
|
|
23
|
+
over declared work and you need step identity, declared file effects, and an
|
|
24
|
+
auditable history as values you can hold, hash, and store.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
|
|
29
|
+
|
|
30
|
+
Compiling asks Effect for its `Crypto` service, which a platform package
|
|
31
|
+
supplies:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @effect/platform-node@4.0.0-rc.115 @effect/platform-node-shared@4.0.0-rc.115
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Node.js 26.4.0 or later, and `effect` 4.0.0-rc.115 as a peer. Recording a plan
|
|
38
|
+
is [`@smthrs/plan-store`](https://plan-store.smithers.sh), which owns the SQL
|
|
39
|
+
and brings [`@smthrs/database`](https://database.smithers.sh) with it.
|
|
40
|
+
|
|
41
|
+
## Compile a plan
|
|
42
|
+
|
|
43
|
+
Two steps, where the second consumes the first's result and reads the file the
|
|
44
|
+
first writes:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
|
|
48
|
+
import * as KeyMaterial from "@smthrs/plan/KeyMaterial"
|
|
49
|
+
import * as Plan from "@smthrs/plan/Plan"
|
|
50
|
+
import * as Effect from "effect/Effect"
|
|
51
|
+
|
|
52
|
+
/** Everything hashed about one node. `sealed` is work whose result may be reused across runs. */
|
|
53
|
+
const sealed = (body: unknown, inputs: ReadonlyArray<KeyMaterial.InputRef> = []): KeyMaterial.KeyMaterial => ({
|
|
54
|
+
version: KeyMaterial.version,
|
|
55
|
+
kind: "sealed",
|
|
56
|
+
body,
|
|
57
|
+
inputs,
|
|
58
|
+
layers: [],
|
|
59
|
+
capabilities: []
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const drafts: ReadonlyArray<Plan.NodeDraft> = [
|
|
63
|
+
{
|
|
64
|
+
id: "run-tests",
|
|
65
|
+
material: sealed({ action: "run-tests" }, [{ _tag: "Ref", from: "read-pr", path: [] }]),
|
|
66
|
+
effects: { reads: ["pr.json"], writes: ["report.json"], boundaryMode: "hard" }
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "read-pr",
|
|
70
|
+
material: sealed({ action: "read-pr", pr: 4821 }),
|
|
71
|
+
effects: { reads: [], writes: ["pr.json"], boundaryMode: "hard" }
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const plan = await Effect.runPromise(
|
|
76
|
+
Plan.compile({ planId: "review-4821", flow: "example/Review", nodes: drafts }).pipe(
|
|
77
|
+
Effect.provide(NodeCrypto.layer),
|
|
78
|
+
Effect.orDie
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
console.log(plan.digest)
|
|
83
|
+
for (const node of plan.nodes) console.log(node.id, node.key, node.dependsOn)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The digests on your machine match these, because a plan is a pure function of
|
|
87
|
+
its declarations. Compiling reads no file, no clock, and no network:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
key1_61edccc875ff69b798e0c3874d167f65ff0a250b9fe67d27558b854fa2832302
|
|
91
|
+
read-pr key1_6353326ab2a03804d6acfe916debc4f04f09f9deb8f0e93c1ebea3cdd983db18 []
|
|
92
|
+
run-tests key1_e58229e3be2f6abf6f818baf8577fe7045edba6f5e88097b633898478da93985 [ 'read-pr' ]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The drafts went in out of order and came back topologically sorted.
|
|
96
|
+
`run-tests` names `read-pr` once, in its key material, and that single
|
|
97
|
+
declaration became both the hashed dependency and the graph edge, so an edge
|
|
98
|
+
and a key can never disagree about what a node consumes. Change `pr: 4821` to
|
|
99
|
+
another number and both keys move; rename `read-pr` to `fetch-pr` and neither
|
|
100
|
+
does.
|
|
101
|
+
|
|
102
|
+
Recording that plan is
|
|
103
|
+
[`@smthrs/plan-store`](https://plan-store.smithers.sh), whose
|
|
104
|
+
[quickstart](https://plan-store.smithers.sh/quickstart/) builds the whole
|
|
105
|
+
composition against an in-memory database.
|
|
106
|
+
|
|
107
|
+
## What is in here
|
|
108
|
+
|
|
109
|
+
| Module | Role |
|
|
110
|
+
| ----------------- | --------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| `Node` | The pure, pipeable authoring AST: `succeed`, `all`, `map`, `andThen`, `branch`, `catch`, `priority` |
|
|
112
|
+
| `Planned` | The strict placeholder a body sees where a step result will be, and the reference it records |
|
|
113
|
+
| `GraphBuildError` | The refusals a plan-time build raises instead of producing a wrong plan |
|
|
114
|
+
| `FileSet` | The static filesystem declaration vocabulary: patterns, globs, tree artifacts, filegroups, overlap |
|
|
115
|
+
| `KeyMaterial` | What a planner declares about a node: body, tagged input references, layers, capabilities, effects |
|
|
116
|
+
| `StepKey` | The compiler that turns material plus resolved dependency digests into a `@smthrs/keys` `Key` |
|
|
117
|
+
| `Plan` | `compile`, `append`, the node/edge/conflict schemas, and the digest an approval binds to |
|
|
118
|
+
| `PlanDiff` | A comparison of two plans as a value: added, removed, re-keyed with attribution, unchanged |
|
|
119
|
+
| `Placement` | Where a node runs: the four directives, their host-selection detail, and the annotation key |
|
|
120
|
+
|
|
121
|
+
Every module is its own entry point, so
|
|
122
|
+
`import * as Plan from "@smthrs/plan/Plan"` keeps a bundle to what you reach
|
|
123
|
+
for. The root entry re-exports all of them as namespaces.
|
|
124
|
+
|
|
125
|
+
## The four rules this package exists to keep
|
|
126
|
+
|
|
127
|
+
**Planning demands nothing.** A `Node` carries Effect's requirement channel,
|
|
128
|
+
`R`, and carries it as a phantom: no combinator here reads it, and the AST, the
|
|
129
|
+
graph, the key material, and every digest are identical whatever it says.
|
|
130
|
+
Building a plan therefore asks for no service at all. What fills the channel is
|
|
131
|
+
a call to something whose code lives elsewhere, an action, so a plan's type
|
|
132
|
+
states which implementations running it will need, and the place that runs it
|
|
133
|
+
is where the compiler asks for them. Each combinator unions its parts: `all`
|
|
134
|
+
over its members, `map` and `andThen` along the chain, `branch` over BOTH arms
|
|
135
|
+
and `catch` over its failure arm, because both arms of a decision are topology
|
|
136
|
+
the plan carries and a run has to be able to take either.
|
|
137
|
+
|
|
138
|
+
**Planning performs no I/O.** Nothing here reads a file, a clock, or a network.
|
|
139
|
+
A node's declared `effects` carry read and write _paths_, never digests, since
|
|
140
|
+
measuring them is the scheduler's run-time job.
|
|
141
|
+
|
|
142
|
+
**Invalidation is re-keying.** A node's key is a function of what it consumes,
|
|
143
|
+
so an edited declaration re-keys that node and its dependent cone and nothing
|
|
144
|
+
else. There is deliberately **no reverse-dependency index and no invalidating
|
|
145
|
+
node visitor**: content addressing subsumes both.
|
|
146
|
+
|
|
147
|
+
**A plan grows; it is never rewritten.** `append` adds a pre-keyed subgraph at
|
|
148
|
+
the next generation. Recorded nodes keep their id, key, edges, and generation
|
|
149
|
+
byte for byte, and the SQL raises rather than letting a caller update or delete
|
|
150
|
+
one. Re-ordering after a reconciliation happens by re-keying _future_ steps.
|
|
151
|
+
Appending to a plan that was never recorded, or over a skipped generation, is a
|
|
152
|
+
`constraint` refusal, because the alternative is node rows for a plan that does
|
|
153
|
+
not exist and that the append-only triggers then forbid removing.
|
|
154
|
+
|
|
155
|
+
## Conflict annotations
|
|
156
|
+
|
|
157
|
+
Declared write sets make overlap detectable at plan time. `compile` annotates
|
|
158
|
+
both members of every overlapping pair that no dependency path already orders:
|
|
159
|
+
`serialize` is the default and gives the later writer an ordering edge that is
|
|
160
|
+
deliberately not key material, so a serialized node keeps its cache hit; `lane`
|
|
161
|
+
gives both writers lane annotations and no edge; `fail` refuses the compile,
|
|
162
|
+
for flows that promise disjointness. Each annotation also carries the runtime
|
|
163
|
+
strategy, `delay-rebase` or `stop-merge`, and both strategies are folded into
|
|
164
|
+
the plan digest a human approves.
|
|
165
|
+
|
|
166
|
+
A node that reads a path another node writes is not a conflict but a missing
|
|
167
|
+
edge: `compile` puts the reader behind its producer by growing `dependsOn`, and
|
|
168
|
+
fails with `cycle` when no edge set can satisfy both that and the declared
|
|
169
|
+
order.
|
|
170
|
+
|
|
171
|
+
[Declared effects and conflicts](https://plan.smithers.sh/concepts/effects-and-conflicts/)
|
|
172
|
+
covers both passes in full.
|
|
173
|
+
|
|
174
|
+
## When it refuses
|
|
175
|
+
|
|
176
|
+
`Plan.compile` and `Plan.append` fail with a `PlanError` carrying one of seven
|
|
177
|
+
stable codes: `cycle`, `unknown_dependency`, `duplicate_node`,
|
|
178
|
+
`overlap_forbidden`, `invalid_effects`, `invalid_node`, and `graph_too_large`.
|
|
179
|
+
`Plan.verify`, which every store read and write runs, adds an eighth,
|
|
180
|
+
`invalid_plan`: the plan decoded but its generations, approval digest, keys,
|
|
181
|
+
effects, or ordering do not match its content. A plan-time build refuses
|
|
182
|
+
through `GraphBuildError`, whose closed code set names the site and the fix;
|
|
183
|
+
`unstable_callback` in that set is raised by `@smthrs/flow` when a stable build
|
|
184
|
+
meets a callback without a `Node.capture` declaration. Persistence has its own
|
|
185
|
+
refusals, all of them
|
|
186
|
+
[`@smthrs/plan-store`](https://plan-store.smithers.sh)'s.
|
|
187
|
+
|
|
188
|
+
[Troubleshooting](https://plan.smithers.sh/troubleshooting/) lists every code
|
|
189
|
+
with the change that clears it.
|
|
190
|
+
|
|
191
|
+
## Paths, payloads, and immutability
|
|
192
|
+
|
|
193
|
+
Declared paths are workspace-relative and compared in one spelling:
|
|
194
|
+
`FileSet.canonical` rewrites every separator to `/` and normalizes to Unicode
|
|
195
|
+
NFC, and `FileSet.workspaceRelative` refuses absolute paths, drive letters,
|
|
196
|
+
`..` and `.` segments, empty segments, the C0 control range, and DEL.
|
|
197
|
+
|
|
198
|
+
A node payload is stored as its inert JSON mirror, so a data-valued `toJSON` is
|
|
199
|
+
honoured while an accessor or an unsupported prototype fails with
|
|
200
|
+
`invalid_payload` rather than executing author code or collapsing distinct
|
|
201
|
+
values onto `{}`. That mirror is identity material: it is hashed into the step
|
|
202
|
+
key and persisted verbatim as plaintext in `node_json` and in the approval card,
|
|
203
|
+
and this package never redacts it. Keep credentials out of payloads. A secret
|
|
204
|
+
belongs in a layer, a capability, or the environment, resolved at dispatch. `Node.succeed(input)` returns `Node<Succeed<typeof input>>`:
|
|
205
|
+
the success type describes that projection. Dates become `string | null`, URLs
|
|
206
|
+
become strings, and callable object members disappear. Planned references
|
|
207
|
+
resolve to their result types. Reconstruct domain objects explicitly in
|
|
208
|
+
`Node.map` when needed. A compiled plan is deep-frozen: mutating the draft objects
|
|
209
|
+
you passed in cannot change the plan, its keys, or its digest.
|
|
210
|
+
|
|
211
|
+
`Plan.maximumPlanNodes` bounds compilation. The conflict and
|
|
212
|
+
reader-after-writer passes compare node pairs, so a plan above that bound is
|
|
213
|
+
refused with `graph_too_large` before any pair is compared.
|
|
214
|
+
|
|
215
|
+
## Browser support
|
|
216
|
+
|
|
217
|
+
Browser-safe: the package resolves no `node:` built-in and binds no database,
|
|
218
|
+
so compiling, diffing, and building node graphs all work in a browser.
|
|
219
|
+
Persistence is a separate package, which is where a platform choice enters.
|
|
220
|
+
|
|
221
|
+
## Links
|
|
222
|
+
|
|
223
|
+
- [Documentation](https://plan.smithers.sh)
|
|
224
|
+
- [API reference](https://plan.smithers.sh/reference/api/)
|
|
225
|
+
- [`@smthrs/plan-store`](https://plan-store.smithers.sh), the append-only store
|
|
226
|
+
a compiled plan is recorded in
|
|
227
|
+
- [`@smthrs/flows`](https://flows.smithers.sh), the whole engine as a single
|
|
228
|
+
dependency, which re-exports this package as its `Plan` namespace
|
|
229
|
+
- [License: MIT](./LICENSE)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The caller-declared cache policy: how far a recorded sealed result may
|
|
3
|
+
* travel, and how long it stays servable.
|
|
4
|
+
*
|
|
5
|
+
* This is the ONE cache-policy model. `@smthrs/flow` publishes it as
|
|
6
|
+
* `CacheEnvironment.CachePolicy` and the engine reads it off a dispatched
|
|
7
|
+
* action; `@smthrs/patterns` declares it over a flow as `WithCache.Policy`. It
|
|
8
|
+
* used to be two declarations in those two packages, tied together by nothing
|
|
9
|
+
* but a re-typed identifier string, so a field added on one side was invisible
|
|
10
|
+
* to the other. It lives here because `@smthrs/plan` is the package both
|
|
11
|
+
* already depend on.
|
|
12
|
+
*
|
|
13
|
+
* The annotation key still reads `"@smthrs/flow/Action/CachePolicy"`.
|
|
14
|
+
* `@smthrs/engine-store`'s `ActionPersistence` reads the policy off stored and
|
|
15
|
+
* dispatched actions by that identifier, so retagging it would make every
|
|
16
|
+
* policy already written invisible at dispatch. The identifier is a wire
|
|
17
|
+
* value, not a location.
|
|
18
|
+
*
|
|
19
|
+
* @since 1.0.0-rc.0
|
|
20
|
+
*/
|
|
21
|
+
import * as Context from "effect/Context";
|
|
22
|
+
import * as Schema from "effect/Schema";
|
|
23
|
+
/**
|
|
24
|
+
* How far a recorded sealed result may travel.
|
|
25
|
+
*
|
|
26
|
+
* `shared` is the content-addressed default: the key names the inputs and the
|
|
27
|
+
* environment, so any run on any host that would have produced the same bytes
|
|
28
|
+
* may reuse the row. `run` and `flow` narrow that on purpose — a step whose
|
|
29
|
+
* result is only meaningful inside one execution, or inside one flow, folds
|
|
30
|
+
* that identity into its key so a sibling never reads it.
|
|
31
|
+
*
|
|
32
|
+
* The old scope vocabulary was `run | workflow | global`; `flow` and `shared`
|
|
33
|
+
* are the same three levels named after the current concepts.
|
|
34
|
+
*
|
|
35
|
+
* @category models
|
|
36
|
+
* @since 1.0.0-rc.0
|
|
37
|
+
*/
|
|
38
|
+
export declare const CacheScope: Schema.Literals<readonly ["run", "flow", "shared"]>;
|
|
39
|
+
/**
|
|
40
|
+
* The value form of {@link CacheScope}.
|
|
41
|
+
*
|
|
42
|
+
* @category models
|
|
43
|
+
* @since 1.0.0-rc.0
|
|
44
|
+
*/
|
|
45
|
+
export type CacheScope = typeof CacheScope.Type;
|
|
46
|
+
/**
|
|
47
|
+
* The caller's declaration about the decay and the reach of a sealed result.
|
|
48
|
+
*
|
|
49
|
+
* `ttlMs` bounds the age of a row the engine may serve: past it the dispatch
|
|
50
|
+
* executes again and journals `cache-expired`, so the refusal is durable
|
|
51
|
+
* evidence a replay reads rather than a fresh clock reading. `scope` decides
|
|
52
|
+
* what the key names besides the inputs.
|
|
53
|
+
*
|
|
54
|
+
* Both fields are optional and both defaults are the pre-policy behavior: no
|
|
55
|
+
* age bound, and the reach the composition's cache environment already
|
|
56
|
+
* granted.
|
|
57
|
+
*
|
|
58
|
+
* @category schemas
|
|
59
|
+
* @since 1.0.0-rc.0
|
|
60
|
+
*/
|
|
61
|
+
export declare const CachePolicy: Schema.Struct<{
|
|
62
|
+
readonly ttlMs: Schema.optionalKey<Schema.Int>;
|
|
63
|
+
readonly scope: Schema.optionalKey<Schema.Literals<readonly ["run", "flow", "shared"]>>;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* The value form of {@link CachePolicy}.
|
|
67
|
+
*
|
|
68
|
+
* @category models
|
|
69
|
+
* @since 1.0.0-rc.0
|
|
70
|
+
*/
|
|
71
|
+
export type CachePolicy = typeof CachePolicy.Type;
|
|
72
|
+
/**
|
|
73
|
+
* Annotation key carrying a declaration's {@link CachePolicy}.
|
|
74
|
+
*
|
|
75
|
+
* It is an annotation, not a field on the declaration, for the same reason
|
|
76
|
+
* placement and effects are: the policy is data that travels with the
|
|
77
|
+
* declaration and is read by whoever executes it, and adding it changes no
|
|
78
|
+
* existing call site.
|
|
79
|
+
*
|
|
80
|
+
* @category annotations
|
|
81
|
+
* @since 1.0.0-rc.0
|
|
82
|
+
*/
|
|
83
|
+
export declare const CachePolicyAnnotation: Context.Service<{
|
|
84
|
+
readonly ttlMs?: number;
|
|
85
|
+
readonly scope?: "run" | "flow" | "shared";
|
|
86
|
+
}, {
|
|
87
|
+
readonly ttlMs?: number;
|
|
88
|
+
readonly scope?: "run" | "flow" | "shared";
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Reads the cache policy an annotation bag carries, or `undefined` when it
|
|
92
|
+
* carries none.
|
|
93
|
+
*
|
|
94
|
+
* @category getters
|
|
95
|
+
* @since 1.0.0-rc.0
|
|
96
|
+
*/
|
|
97
|
+
export declare const cachePolicyOf: (annotations: Context.Context<never>) => CachePolicy | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Attaches a cache policy to a declaration that can carry annotations.
|
|
100
|
+
*
|
|
101
|
+
* The declaration is not mutated: `annotate` answers a new one carrying the
|
|
102
|
+
* policy, which is what a plan captures. It is written against the `annotate`
|
|
103
|
+
* method rather than against an action or a flow type so the one declaration
|
|
104
|
+
* serves both, which is what `@smthrs/flow` publishes as
|
|
105
|
+
* `CacheEnvironment.withCache`.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* import { CachePolicy } from "@smthrs/plan"
|
|
110
|
+
* import { Action } from "@smthrs/flow"
|
|
111
|
+
* import * as Effect from "effect/Effect"
|
|
112
|
+
* import * as Schema from "effect/Schema"
|
|
113
|
+
*
|
|
114
|
+
* const compile = CachePolicy.annotate(
|
|
115
|
+
* Action.make({
|
|
116
|
+
* name: "build/compile",
|
|
117
|
+
* success: Schema.String,
|
|
118
|
+
* tier: "sealed",
|
|
119
|
+
* execute: Effect.succeed("dist/server.js")
|
|
120
|
+
* }),
|
|
121
|
+
* { ttlMs: 60_000, scope: "shared" }
|
|
122
|
+
* )
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @category combinators
|
|
126
|
+
* @since 1.0.0-rc.0
|
|
127
|
+
*/
|
|
128
|
+
export declare const annotate: <A extends {
|
|
129
|
+
annotate: (key: typeof CachePolicyAnnotation, value: CachePolicy) => A;
|
|
130
|
+
}>(declaration: A, policy: CachePolicy) => A;
|
|
131
|
+
//# sourceMappingURL=CachePolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CachePolicy.d.ts","sourceRoot":"","sources":["../../src/CachePolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AAEzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,qDAA6C,CAAA;AAEpE;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAU/C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW;;;EAGtB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;;;EAAkE,CAAA;AAEpG;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,aAAa,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAG,WAAW,GAAG,SACJ,CAAA;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS;IAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,qBAAqB,EAAE,KAAK,EAAE,WAAW,KAAK,CAAC,CAAA;CAAE,EAC3G,aAAa,CAAC,EACd,QAAQ,WAAW,KAClB,CAAwD,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var CachePolicy_exports = {};
|
|
30
|
+
__export(CachePolicy_exports, {
|
|
31
|
+
CachePolicy: () => CachePolicy,
|
|
32
|
+
CachePolicyAnnotation: () => CachePolicyAnnotation,
|
|
33
|
+
CacheScope: () => CacheScope,
|
|
34
|
+
annotate: () => annotate,
|
|
35
|
+
cachePolicyOf: () => cachePolicyOf
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(CachePolicy_exports);
|
|
38
|
+
var Context = __toESM(require("effect/Context"), 1);
|
|
39
|
+
var Option = __toESM(require("effect/Option"), 1);
|
|
40
|
+
var Schema = __toESM(require("effect/Schema"), 1);
|
|
41
|
+
const CacheScope = Schema.Literals(["run", "flow", "shared"]);
|
|
42
|
+
const PositiveMillis = Schema.Int.check(Schema.isGreaterThan(0));
|
|
43
|
+
const CachePolicy = Schema.Struct({
|
|
44
|
+
ttlMs: Schema.optionalKey(PositiveMillis),
|
|
45
|
+
scope: Schema.optionalKey(CacheScope)
|
|
46
|
+
});
|
|
47
|
+
const CachePolicyAnnotation = Context.Service("@smthrs/flow/Action/CachePolicy");
|
|
48
|
+
const cachePolicyOf = (annotations) => Option.getOrUndefined(Context.getOption(annotations, CachePolicyAnnotation));
|
|
49
|
+
const annotate = (declaration, policy) => declaration.annotate(CachePolicyAnnotation, policy);
|
|
50
|
+
//# sourceMappingURL=CachePolicy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/CachePolicy.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The caller-declared cache policy: how far a recorded sealed result may\n * travel, and how long it stays servable.\n *\n * This is the ONE cache-policy model. `@smthrs/flow` publishes it as\n * `CacheEnvironment.CachePolicy` and the engine reads it off a dispatched\n * action; `@smthrs/patterns` declares it over a flow as `WithCache.Policy`. It\n * used to be two declarations in those two packages, tied together by nothing\n * but a re-typed identifier string, so a field added on one side was invisible\n * to the other. It lives here because `@smthrs/plan` is the package both\n * already depend on.\n *\n * The annotation key still reads `\"@smthrs/flow/Action/CachePolicy\"`.\n * `@smthrs/engine-store`'s `ActionPersistence` reads the policy off stored and\n * dispatched actions by that identifier, so retagging it would make every\n * policy already written invisible at dispatch. The identifier is a wire\n * value, not a location.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Context from \"effect/Context\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\n\n/**\n * How far a recorded sealed result may travel.\n *\n * `shared` is the content-addressed default: the key names the inputs and the\n * environment, so any run on any host that would have produced the same bytes\n * may reuse the row. `run` and `flow` narrow that on purpose \u2014 a step whose\n * result is only meaningful inside one execution, or inside one flow, folds\n * that identity into its key so a sibling never reads it.\n *\n * The old scope vocabulary was `run | workflow | global`; `flow` and `shared`\n * are the same three levels named after the current concepts.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport const CacheScope = Schema.Literals([\"run\", \"flow\", \"shared\"])\n\n/**\n * The value form of {@link CacheScope}.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type CacheScope = typeof CacheScope.Type\n\n/**\n * A positive whole number of milliseconds.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nconst PositiveMillis = Schema.Int.check(Schema.isGreaterThan(0))\n\n/**\n * The caller's declaration about the decay and the reach of a sealed result.\n *\n * `ttlMs` bounds the age of a row the engine may serve: past it the dispatch\n * executes again and journals `cache-expired`, so the refusal is durable\n * evidence a replay reads rather than a fresh clock reading. `scope` decides\n * what the key names besides the inputs.\n *\n * Both fields are optional and both defaults are the pre-policy behavior: no\n * age bound, and the reach the composition's cache environment already\n * granted.\n *\n * @category schemas\n * @since 1.0.0-rc.0\n */\nexport const CachePolicy = Schema.Struct({\n ttlMs: Schema.optionalKey(PositiveMillis),\n scope: Schema.optionalKey(CacheScope)\n})\n\n/**\n * The value form of {@link CachePolicy}.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type CachePolicy = typeof CachePolicy.Type\n\n/**\n * Annotation key carrying a declaration's {@link CachePolicy}.\n *\n * It is an annotation, not a field on the declaration, for the same reason\n * placement and effects are: the policy is data that travels with the\n * declaration and is read by whoever executes it, and adding it changes no\n * existing call site.\n *\n * @category annotations\n * @since 1.0.0-rc.0\n */\nexport const CachePolicyAnnotation = Context.Service<CachePolicy>(\"@smthrs/flow/Action/CachePolicy\")\n\n/**\n * Reads the cache policy an annotation bag carries, or `undefined` when it\n * carries none.\n *\n * @category getters\n * @since 1.0.0-rc.0\n */\nexport const cachePolicyOf = (annotations: Context.Context<never>): CachePolicy | undefined =>\n Option.getOrUndefined(Context.getOption(annotations, CachePolicyAnnotation))\n\n/**\n * Attaches a cache policy to a declaration that can carry annotations.\n *\n * The declaration is not mutated: `annotate` answers a new one carrying the\n * policy, which is what a plan captures. It is written against the `annotate`\n * method rather than against an action or a flow type so the one declaration\n * serves both, which is what `@smthrs/flow` publishes as\n * `CacheEnvironment.withCache`.\n *\n * @example\n * ```ts\n * import { CachePolicy } from \"@smthrs/plan\"\n * import { Action } from \"@smthrs/flow\"\n * import * as Effect from \"effect/Effect\"\n * import * as Schema from \"effect/Schema\"\n *\n * const compile = CachePolicy.annotate(\n * Action.make({\n * name: \"build/compile\",\n * success: Schema.String,\n * tier: \"sealed\",\n * execute: Effect.succeed(\"dist/server.js\")\n * }),\n * { ttlMs: 60_000, scope: \"shared\" }\n * )\n * ```\n *\n * @category combinators\n * @since 1.0.0-rc.0\n */\nexport const annotate = <A extends { annotate: (key: typeof CachePolicyAnnotation, value: CachePolicy) => A }>(\n declaration: A,\n policy: CachePolicy\n): A => declaration.annotate(CachePolicyAnnotation, policy)\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBA,cAAyB;AACzB,aAAwB;AACxB,aAAwB;AAiBjB,MAAM,aAAa,OAAO,SAAS,CAAC,OAAO,QAAQ,QAAQ,CAAC;AAgBnE,MAAM,iBAAiB,OAAO,IAAI,MAAM,OAAO,cAAc,CAAC,CAAC;AAiBxD,MAAM,cAAc,OAAO,OAAO;AAAA,EACvC,OAAO,OAAO,YAAY,cAAc;AAAA,EACxC,OAAO,OAAO,YAAY,UAAU;AACtC,CAAC;AAqBM,MAAM,wBAAwB,QAAQ,QAAqB,iCAAiC;AAS5F,MAAM,gBAAgB,CAAC,gBAC5B,OAAO,eAAe,QAAQ,UAAU,aAAa,qBAAqB,CAAC;AAgCtE,MAAM,WAAW,CACtB,aACA,WACM,YAAY,SAAS,uBAAuB,MAAM;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|