Repository navigation
A value in an any-typed slot loses its concrete type at save; the document must carry it #712
Description
Epic: #443 — Serialization and the single codec
Related: #711 (declared-type numbers), #709 (conversion rules)
A parameter declared any has no target type to read a serialized value against. Unlike #711, this cannot
be fixed by reading more carefully — the information is destroyed at save, before anything reloads.
The mechanism
json.Marshal(float64(42)) emits 42. Go's encoder writes the shortest representation that round-trips as a
float64, and for an integral value that has no decimal point. So a Starlark float 42.0 landing in a
map[string]any is written to the document as 42, indistinguishable from an integer.
UseNumber on the way back in preserves the literal faithfully. The literal simply no longer says "float".
yaml.v3 marshals float64(42) as 42 as well, so this is not a JSON-specific defect.
It is live
any appears in parameters across nine files, including ones that certainly reach a serialized graph:
plan.Variable(name string, defaultValue any, field string)
plan.Plan(name op.ActionName, args []any, kwargs map[string]any)
template.RenderText(content string, data map[string]any)
function.Call(callable Resource, args []any, kwargs map[string]any)plan.Variable's defaultValue is the clearest case: a declared default of 1.0 is written as 1 and
reloads as an integer.
Ruling
For a value whose slot is
any, the document stores the type. There is no other option: with no
declared type at either end, the document is the only place the information can live.
This does not apply to a value whose parameter has a declared type. There the field says how to read the
number, which is what #711 implements — fs.FileMode wants an integer and the text answers. Only any slots
need the type written down.
What this opens
- The envelope's shape.
{"$type": "float", "value": 42}is the obvious form, and it changes every
document containing ananyslot. A narrower alternative — force a decimal point when marshaling a float
into ananyslot, so42.0stays42.0— is smaller but only covers numbers, and the same question
arises for any other type whose identity JSON cannot carry. - Which types need it. Numbers are the case in hand. Strings, bools, lists, and dicts survive JSON
unambiguously. Anything with a Go type that JSON flattens is a candidate. - Checksums and graph identity. Changing what a document contains changes the bytes. Whether it changes
the canonical form is a separate question, and one JSON reload turns every authored integer into a float64, and the codecs disagree #711 is already having to answer. - Read compatibility. Greenfield, per the repository's governing principle, so old documents need no
accommodation — but that should be a stated decision rather than an assumption.
Not blocking #711
#711 narrows to parameters with a declared type. any-typed slots keep today's behavior until this is
decided, so the two do not contend.
Acceptance
- A float in an
anyslot reloads as a float - An integer in an
anyslot reloads as an integer -
plan.Variable(default=1.0)survives save and reload with its type intact - The envelope is documented as part of the document format, not left implicit in the encoder
- JSON and YAML agree
Plan and design documents
| Kind | Document |
|---|---|
| Plan | docs/plans/fix/712-any-slot-types.md |
| Design | docs/architecture/5-graph-trace-integrity.md |
| Design | docs/architecture/2.1-typed-slots.md |
| Design | docs/architecture/5.2-recovery-serialization.md |
Activity
fix(op): a reloaded number is read against the field it fills
A reloaded number is read against the field it fills (#713)
David-Noble-at-work commentedon Aug 30, 2026
Ruled: a node carries its graph's catalog
There was no fork here. Nodes are constructed with the graph during planning, so a node carries the catalog
its graph owns. I presented three options as though the design were open; it is not, and two of them were
wrong for reasons the design already settles:
NewGraphand storing the id instead of the Resource changes what dispatch sees, tosolve a serialization problem. §5.6 and the
Pending/Activerules are built on the slot holding theresource.
Drift between two renderings is exactly what devlore-test is broken: its primary artifact is empty, its streams are inverted, and its own tests cannot detect either #738 turned out to be.
Where it lands
buildGraph(pkg/op/graph.go:214-216) already walksg.root.descendantNodes()to buildunitsByID, andit holds
resourceCatalog. That loop is where the graph takes ownership of its nodes, so it is where eachnode receives the catalog. No new traversal, no new seam.
What it fixes
encodeResourcecurrently readsresource.RuntimeEnvironment().ResourceCatalog-- the resource's ownenvironment, which in the executor tests has no catalog at all. It must read the graph's, as
Graph.resourceCatalog's doc comment already requires: "every later session-owner (a Go-sideGraphExecutor.Run, a serializer, an inspector) reads fromresourceCatalograther than from thelong-gone planning environment."
Three executor tests fail today on
$resource ... has no catalog to name it, plusTestLoadGraph_AResourceInAnAnySlotReloadsAsAResource. All four are the same defect.Remaining work on the read side
assembleUnits/assembleNode/assembleBindingscurrently threadenv.ResourceCatalog. That is likelyalso wrong: the graph's own catalog is reconstructed by
unpackCatalogfrom the document'sresourcessection, and that -- not the ambient environment's -- is what a slot's id must resolve against.
This is recorded here rather than in
docs/plans/any-slot-types.mdbecause the plan lives on theany-slot-typesbranch. It should be folded into the plan's design decisions when that branch is nextchecked out.
Merge develop: the output convention, and the devlore-test rewrite (#740
fix(result): a table row survives its own newlines, and jq survives i…
Every presentation is a presentation of the JSON: formatting rules, l…
A decoded number with nothing to read it against is a load error, and…
Receipts and the recovery stack envelope their results and slots, and…
Resource slots record the catalog id and the checksum covers the cata…
Annotations and variables carry their types, and the codec accepts Go…
A bare value or an unknown envelope in an any slot is refused, and th…
Pin #735's acceptance: a slot binds its own generation, both seams ag…
A value in an any slot carries its type, a resource slot records the …