Skip to content

Commit 5cf5889

Browse files
committed
spec(runner): updated plan for DX1 & adjusted specs
1 parent 3055c6b commit 5cf5889

File tree

7 files changed

+371
-230
lines changed

7 files changed

+371
-230
lines changed

docs/specs/recipe-construction/capability-wrappers.md

Lines changed: 89 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
# Capability Wrappers Prototype
1+
# Capability Types via Branded Types
2+
3+
> **Status:** This design has been **superseded by the branded types approach**
4+
> described in `rollout-plan.md`. We are using TypeScript branded types with
5+
> symbol-based brands rather than runtime proxy wrappers.
26
37
## Objectives
48

5-
- Collapse `OpaqueRef` and runtime `Cell` APIs into a single object whose
9+
- Collapse `OpaqueRef` and runtime `Cell` APIs into a single type system whose
610
surface area is gated by explicit capabilities (`Opaque`, `Readonly`,
711
`Mutable`, `Writeonly`).
12+
- Use TypeScript's type system (branded types) rather than runtime Proxy objects
13+
to enforce capability boundaries.
814
- Keep recipe ergonomics intact by continuing to expose property proxies and
9-
helpers like `.map()` while steering operations through capability-aware
10-
wrappers.
15+
helpers like `.map()`.
1116
- Provide a migration path that lets existing recipes run during the migration
1217
window by adapting the current builder implementation incrementally.
1318

@@ -27,84 +32,99 @@
2732
nested recipes. That indirection means wrappers must cooperate with module
2833
factories that expect builder-time proxies yet execute against runtime cells.
2934

30-
## Wrapper Shape
31-
32-
- Introduce a lightweight `CapabilityCell<T, C extends Capability>` interface
33-
that wraps a concrete runtime cell and exposes operations allowed by
34-
capability `C`. Capabilities map to subsets of the current `Cell` API:
35-
- `Opaque`: property proxies, `.map`, `.filter`, `.derive`, structural
36-
equality, read-only helpers.
37-
- `Readonly`: everything in `Opaque` plus `.get`, `.getAsQueryResult`, and
38-
schema navigation helpers that do not mutate state.
39-
- `Mutable`: superset of `Readonly` plus `.set`, `.update`, `.push`, and
40-
`.redirectTo`.
41-
- `Writeonly`: `.send`, `.set`, `.redirectTo`, but intentionally hides `.get`
42-
to encourage event-sink authoring disciplines.
43-
- Use `Proxy` objects to intercept property access and method calls. Each proxy
44-
lazily constructs child proxies with the same capability so nested lookups
45-
remain ergonomic (e.g. `mutable.todos[0].title.set("…")`).
46-
- When a helper like `.map` is invoked, delegate to a capability-aware shim that
47-
wraps the callee recipe factory so the inner function receives wrappers
48-
instead of raw cells or `OpaqueRef`s. Internally reuse the existing
49-
`createNodeFactory` path to avoid reimplementing module registration.
50-
51-
## Construction Flow
52-
53-
1. Builder entry points (`recipe`, `lift`, `handler`) push a frame, wrap inputs
54-
in `CapabilityCell` proxies instead of `opaqueRef`, and still record the
55-
graph using existing traversal utilities.
56-
2. For compatibility, supply adapters that allow legacy `OpaqueRef` helper
57-
affordances (`setDefault`, `unsafe_bindToRecipeAndPath`) to continue working
58-
until all call sites migrate. These adapters simply forward to the wrapped
59-
runtime cell or translate to snapshot metadata updates.
60-
3. During runtime execution, `pushFrameFromCause` already seeds the frame with
61-
the `cause` and `unsafe_binding`. Wrappers created while executing a lifted
62-
function can therefore call `runtime.getCell` immediately because the cause
63-
data is ready.
35+
## Type System Shape (Branded Types Approach)
36+
37+
The actual implementation uses **branded types** rather than runtime Proxy wrappers:
38+
39+
- Create a `CellLike<T>` type with a symbol-based brand where the value is
40+
`Record<string, boolean>` representing capability flags.
41+
- Factor out interface parts along: reading, writing, `.send` (for stream-like),
42+
and derives (currently just `.map`).
43+
- Define capability types by combining these factored parts with specific brand
44+
configurations:
45+
- `OpaqueRef<T>`: `{ opaque: true, read: false, write: false, stream: false }`
46+
- Supports: property proxies, `.map`, `.filter`, `.derive`, structural equality
47+
- `Cell<T>` (Mutable): `{ opaque: false, read: true, write: true, stream: true }`
48+
- Supports: everything (`.get`, `.set`, `.update`, `.push`, `.redirectTo`, `.send`)
49+
- `Stream<T>`: `{ opaque: false, read: false, write: false, stream: true }`
50+
- Supports: `.send` only
51+
- `ReadonlyCell<T>`: `{ opaque: false, read: true, write: false, stream: false }`
52+
- Supports: `.get`, `.getAsQueryResult`, schema navigation
53+
- `WriteonlyCell<T>`: `{ opaque: false, read: false, write: true, stream: false }`
54+
- Supports: `.set`, `.update`, `.redirectTo` but hides `.get`
55+
- For `OpaqueRef`, keep proxy behavior where each key access returns another
56+
`OpaqueRef`.
57+
- Simplify most wrap/unwrap types to use `CellLike`.
58+
59+
### Comparison to Original Proxy Design
60+
61+
The branded types approach provides compile-time safety without runtime overhead.
62+
The original proxy-based `CapabilityCell<T, C>` design is **not being implemented**
63+
because TypeScript's type system can enforce the same boundaries more efficiently.
64+
65+
## Construction Flow (Branded Types)
66+
67+
1. Builder entry points (`recipe`, `lift`, `handler`) push a frame and work with
68+
the unified `CellLike` types instead of separate `opaqueRef` and `Cell` types.
69+
2. Cell creation is deferred - cells can be created without an immediate link,
70+
using `.for(cause)` to establish the link later.
71+
3. For compatibility during migration, legacy `OpaqueRef` helper affordances
72+
(`setDefault`, `unsafe_bindToRecipeAndPath`) continue working until all call
73+
sites migrate.
74+
4. During runtime execution, `pushFrameFromCause` seeds the frame with the
75+
`cause` and `unsafe_binding`. Created cells can call `runtime.getCell` when
76+
their cause is ready (either automatically derived or explicitly set via
77+
`.for()`).
6478

6579
## Type System Notes
6680

67-
- Export capability-specific TypeScript types from `@commontools/api` such as
68-
`OpaqueCell<T>`, `ReadonlyCell<T>`, `MutableCell<T>`, and `WriteonlyCell<T>`.
69-
These extend a shared base that keeps the proxy type information available to
70-
recipe authors.
81+
- Export capability-specific TypeScript types from `@commontools/api`:
82+
`OpaqueRef<T>`, `Cell<T>`, `ReadonlyCell<T>`, `WriteonlyCell<T>`, and
83+
`Stream<T>`.
84+
- All types extend a shared `CellLike<T>` base with branded capability flags.
7185
- Extend our JSON Schema annotations so authors can declare capabilities at any
7286
depth. When `asCell: true` is present, allow an `opaque`, `readonly`, or
7387
`writeonly` flag (or the closest JSON Schema standard equivalent if one
74-
exists). Builder proxies read these flags to choose the capability for the
75-
proxy returned by `key()` or nested property access.
88+
exists).
7689
- Provide conditional helper types to map schema metadata to helper surfaces
7790
(e.g., array helpers only appear when `T` extends `readonly any[]`). Reuse the
7891
IFC-aware schema lookup utilities to keep helper availability aligned with the
7992
JSON schema.
80-
- Augment builders so `recipe` factories default to `OpaqueCell` inputs while
93+
- Augment builders so `recipe` factories default to `OpaqueRef` inputs while
8194
`lift` can declare stronger capabilities for each argument via a typed options
8295
bag (e.g., `{ inputs: { item: Capability.Mutable } }`).
8396

84-
## Cause Defaults
85-
86-
- Capability helpers (e.g., `.map`, `.filter`) should emit deterministic
87-
default causes that combine the parent cell's cause with a helper-specific
88-
label. Authors can still pass explicit `cause` overrides to `lift` or
89-
`handler`, but most call sites inherit stable defaults automatically.
90-
- Expose an optional `.setCause(cause: CauseDescriptor)` chain on newly created
91-
capability cells. It overrides the derived id before the cell participates in
92-
the graph. If the cell has already been connected to a node or materialized
93-
into a runtime cell, `.setCause` must throw to avoid inconsistencies.
94-
95-
## Migration Strategy
96-
97-
- Step 1: Introduce wrappers with shims that forward to existing opaque ref
98-
behavior. Dual-write metadata (`export()`) so `factoryFromRecipe` can continue
99-
serializing the graph until the snapshot pipeline lands.
100-
- Step 2: Convert builtin helpers and modules to consume wrappers. Audit
101-
`packages/runner/src/builder` to replace direct `OpaqueRef` imports with
102-
wrapper types while keeping `opaqueRef` available as a thin adapter.
103-
- Step 3: Update recipes in `recipes/` iteratively. Provide codemods that swap
104-
`cell()` usage for capability-specific constructors when explicit mutability is
105-
required.
106-
- Step 4: Remove legacy-only APIs (`setDefault`, `setPreExisting`) once the
107-
snapshot work replaces path-based aliasing and defaults come from schemas.
97+
## Cause Defaults and `.for()` Method
98+
99+
- Cause assignment happens in two layers:
100+
1. **Automatic derivation**: Default causes are derived from frame context,
101+
input cell ids, and implementation fingerprints (see `cause-derivation.md`)
102+
2. **Explicit override via `.for()`**: Authors can call `.for(cause)` to
103+
explicitly assign a cause before the cell is linked
104+
- The `.for()` method provides an explicit layer on top of automation:
105+
- Optional second parameter makes it flexible (ignores if link already exists,
106+
adds extension if cause already exists)
107+
- Throws if cell already connected to a node or materialized into runtime cell
108+
- Helpers (e.g., `.map`, `.filter`) use automatic derivation by default but can
109+
be overridden with explicit `cause` parameters to `lift` or `handler`.
110+
111+
## Migration Strategy (In-Place)
112+
113+
This is an **in-place migration** rather than a V1/V2 opt-in system:
114+
115+
- **Step 1**: Unify Cell API types using branded types (see `rollout-plan.md`)
116+
- Create `CellLike<>` and factor out capability traits
117+
- Remove `ShadowRef`/`unsafe_` mechanisms
118+
- **Step 2**: Enable deferred cell creation with `.for()` method
119+
- Change `RegularCell` constructor to make link optional
120+
- Add `.for()` method for explicit cause assignment
121+
- Implement automatic cause derivation as baseline
122+
- **Step 3**: Update recipe lifecycle to use deferred execution
123+
- Run recipes like lifts with tracked cell/cause creation
124+
- Remove JSON recipe representation
125+
- **Step 4**: Cleanup legacy APIs
126+
- Remove `setDefault`, `setPreExisting` once defaults come from schemas
127+
- Deprecate path-based aliasing in favor of graph snapshots (Phase 2)
108128

109129
## Open Questions
110130

docs/specs/recipe-construction/cause-derivation.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ derive deterministic causes for the **new cells** produced inside that frame by
1010
combining the frame cause with deterministic fingerprints of the inputs and
1111
implementation.
1212

13+
## Two-Layer Approach
14+
15+
Cause assignment works in two complementary layers:
16+
17+
1. **Automatic derivation** (this spec): Default causes are automatically derived
18+
from frame context, input cell ids, and implementation fingerprints
19+
2. **Explicit override via `.for()`**: Authors can call `.for(cause)` on cells to
20+
explicitly assign a cause, providing an explicit layer on top of the automation
21+
22+
The `.for()` method acts as an escape hatch when automatic derivation isn't
23+
sufficient or when authors want stable, human-readable cause names.
24+
1325
## Frame Cause Setup
1426

1527
1. When a lift or handler is invoked at runtime, the runtime computes a
@@ -55,14 +67,22 @@ const cause = createRef({
5567

5668
The resulting entity id seeds the cause for the cell returned by the helper.
5769

58-
### Explicit Overrides
59-
60-
- Newly created capability cells expose `.setCause(cause: CauseDescriptor)` to
61-
replace the derived cause with an explicit value (useful for hand-authored
62-
stability keys). The override must occur **before** the cell participates in
63-
the graph. If the cell has been connected to a node, read, or written, the
64-
call must throw so we do not retroactively change ids that other nodes may
65-
already reference.
70+
### Explicit Overrides via `.for()`
71+
72+
- Newly created cells expose `.for(cause, flexible?)` to replace or refine the
73+
derived cause with an explicit value:
74+
- **Basic usage**: `.for(cause)` assigns the specified cause
75+
- **Flexible mode**: `.for(cause, true)` provides flexibility:
76+
- Ignores the `.for()` if link already exists
77+
- Adds extension if cause already exists (see tracker in `rollout-plan.md`
78+
lines 39-46)
79+
- The override must occur **before** the cell is materialized into a runtime
80+
cell or connected to a node
81+
- If the cell has already been connected, the call must throw to avoid
82+
inconsistencies
83+
- This provides an **explicit layer on top of automatic derivation** - authors
84+
can use automatic derivation for most cells and only call `.for()` when they
85+
need specific control.
6686

6787
## Propagating Causes to Nested Frames
6888

docs/specs/recipe-construction/graph-snapshot.md

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Graph Snapshot Schema
22

3+
> **Status:** This work is **deferred to Phase 2** (see `rollout-plan.md` and
4+
> `overview.md`). The snapshot format described here will be implemented as part
5+
> of the `process` metadata work mentioned in the rollout plan (lines 54-61).
6+
37
## Motivation
48

59
- `Runner.setupInternal` currently stores recipe metadata inside the process
@@ -15,6 +19,19 @@
1519
this graph look like" without re-running the factory, simplifying rehydration
1620
and teardown.
1721

22+
## Relationship to Rollout Plan
23+
24+
This spec describes the implementation for tasks in `rollout-plan.md` lines
25+
54-61:
26+
27+
- Write metadata into result cell including `source` (from context) and
28+
`process` metadata
29+
- The `process` field contains the graph snapshot described in this document,
30+
including:
31+
- The `Module` (which includes the `Program` as link using `cid:hash`)
32+
- Created cells with links to module and/or schema
33+
- History of previously used schemas (P2 for safe updating)
34+
1835
## Snapshot Envelope
1936

2037
Store an additional `graph` payload on the result cell alongside the existing
@@ -82,40 +99,48 @@ type EventHandlerDescriptor = ReactiveModuleDescriptor & { handler: true };
8299
schemas so reactive nodes can express tuple-style inputs used by lifts and
83100
handlers.
84101

85-
## Generation Flow
102+
## Generation Flow (Phase 2)
86103

87-
1. When `Runner.startWithTx` iterates `recipe.nodes`, instrument each
88-
`instantiateNode` call to record:
89-
- The resolved module descriptor, including implementation reference.
90-
- Normalized input links (aliases already resolved by the capability
91-
wrappers so `unwrapOneLevelAndBindtoDoc` is no longer needed).
104+
> **Note:** This describes future implementation work after Phase 1 type
105+
> unification is complete.
106+
107+
1. When `Runner.startWithTx` iterates nodes during lift/handler execution,
108+
instrument each instantiation to record:
109+
- The resolved module descriptor, including implementation reference
110+
- Normalized input links (already resolved since cells have concrete causes
111+
from Phase 1 work)
92112
- Either an output link (for reactive nodes) or a stream link (for event
93-
handlers), whichever applies.
94-
- Optional argument/result schemas attached by the builder.
113+
handlers), whichever applies
114+
- Optional argument/result schemas attached by the builder
95115
2. Compute the final snapshot object, attach it to
96-
`resultCell.withTx(tx).setMetadata("graph", snapshot)`, and persist it in the
97-
same transaction that finishes setup.
116+
`resultCell.withTx(tx).setMetadata("process", snapshot)` (note: using
117+
`process` not `graph` to match rollout plan), and persist it in the same
118+
transaction that finishes setup
119+
120+
## Rehydration Strategy (Phase 2)
98121

99-
## Rehydration Strategy
122+
> **Note:** Deferred to Phase 2 implementation.
100123
101124
- On `Runner.setupInternal`, if a prior snapshot exists, load it before
102125
unpacking defaults. The runtime can:
103126
- Reattach scheduler subscriptions by walking the nodes, reusing modules whose
104-
implementation reference matches.
105-
- Rehydrate handler streams directly from their stored links.
127+
implementation reference matches
128+
- Rehydrate handler streams directly from their stored links
106129
- When a handler or lift causes a graph to rebuild, diff the previous and new
107130
node lists. Nodes that disappear are torn down by following their stored
108131
output/stream links. Nodes with matching descriptors can reuse their existing
109-
cells.
132+
cells
133+
134+
## Teardown and Diffing (Phase 2)
110135

111-
## Teardown and Diffing
136+
> **Note:** Deferred to Phase 2 implementation.
112137
113138
- Nodes uniquely define the dependencies; links are inferred from the cell links
114139
embedded inside each snapshot entry. Diffing node descriptors is sufficient to
115-
drive teardown.
140+
drive teardown
116141
- Maintain a monotonic `generation` counter on the snapshot. When a handler
117142
triggers rehydration, increment the counter so logs can correlate actions with
118-
rebuilds.
143+
rebuilds
119144

120145
## Outstanding Questions
121146

docs/specs/recipe-construction/implementation-plan-v2.md

Lines changed: 0 additions & 54 deletions
This file was deleted.

0 commit comments

Comments
 (0)