Skip to content

client: the envelope form of a program, a surface over the $proc form (coeval M7) - #133

Merged
mark1russell7 merged 1 commit into
mainfrom
feat/envelope-programs
Oct 21, 2026
Merged

mark1russell7 merged 1 commit into
mainfrom
feat/envelope-programs

Conversation

@mark1russell7

Copy link
Copy Markdown
Owner

The envelope form of a program (coeval M7)

coeval decided (D6, D9, D10) that a document is an envelope { type, value } (header keys name, own), a call is an envelope whose type is a procedure path (an array of keys) and whose value is the input, and a reference is { "type": "ref", "value": [root, step, ...] }.

This PR adds the envelope form as a surface that converts to the native $proc form. The interpreter (hydrateInput) does not change. The root of a program decides its form: an envelope inside the input of a $proc program stays plain data, so no existing program changes behavior.

What changes

  • packages/client/src/procedures/envelope.ts (new): isEnvelopeProgram, fromEnvelopeProgram, toEnvelopeProgram, and the types EnvelopeProgram, EnvelopeRef, EnvelopeLiteral, EnvelopePath. Exported from ., ./browser and ./procedures.
  • Client.exec() / Client.execStream() accept an envelope root. It converts first, then runs as before. exec(program, input) merges input exactly as for a $proc root. Every other form and error message is unchanged.
  • PROGRAM-FORMAT.md: new section 11 "The envelope form" (mapping table, a full example, the rules). "Open questions" becomes section 12, and its references are updated (two in the document, one in deep-dive-2026-10/STATUS.md).
  • Changeset: patch of the client group (additive, D13).

The mapping

Envelope form $proc form
{ "type": ["client","add"], "value": input } { "$proc": ["client","add"], "input": input }
{ "type": { "type": "ref", "value": ["world","client","add"] }, "value": input } same
{ "type": path, "value": null } { "$proc": path } (no input: the procedure gets undefined, and passThrough applies)
header "name": "sum" "$name": "sum"
{ "type": "ref", "value": ["names","sum","total"] } { "$ref": "sum.total" }
{ "type": "ref", "value": ["names","last"] } { "$ref": "$last" }
{ "type": "literal", "value": x } { "$literal": x }
an object with a $proc, $ref or $literal key, or a runtime ProcedureRef { "$literal": object } (data)
(none) $when: a program that needs it keeps the $proc form

Errors: a ref root other than names (the message names the root and says that client programs read only names), a ref key with a dot or a key that is not an integer of 0 or more, a header on a ref or a literal, own on a call, a non-string name, a call type that is a ref but not into the world, and cycles or depth over MAX_DATA_DEPTH.

toEnvelopeProgram refuses $when, extra keys on a call or a $ref, a $ref to a step named last, and data that the envelope form reads differently when that data holds a construct. The round trip fromEnvelopeProgram(toEnvelopeProgram(p)) deep-equals p, with two documented exceptions that give the same values: a call with input: null, and envelope-shaped (or $-keyed) data. Both come back wrapped in $literal.

Tests

envelope.test.ts (45 tests): the PROGRAM-FORMAT examples in both forms (strict conversion both ways, and the same results through exec), procedure.define with an envelope body, world-ref types, exec(program, input), execStream, a call without input and passThrough, the escapes, old $proc programs keeping envelopes as data, the errors, and a fast-check property: when toEnvelopeProgram accepts a program, the round trip hydrates to the same value. At 5000 runs that is about 4500 accepted programs, about 2250 of them with calls.

test/api/public-api.txt was updated on purpose with UPDATE_API_SNAPSHOT=1 (the snapshot test only). The update adds the new exports and widens the exec and execStream parameter.

Local runs: pnpm --filter @procedures/client build, typecheck and test (2112 passed), node .github/scripts/typecheck-tests.mjs client, and pnpm lint:ste (0 errors; the warnings were already on main).

🤖 Generated with Claude Code

… (coeval M7)

coeval decided (D6, D9, D10) that a document is an envelope { type, value }
with the optional header keys name and own; a call is an envelope whose
type is a procedure path (an array of keys) and whose value is the input;
a reference is { type: "ref", value: [root, step, ...] }.

- procedures/envelope.ts: isEnvelopeProgram, fromEnvelopeProgram and
  toEnvelopeProgram, and the types EnvelopeProgram, EnvelopeRef,
  EnvelopeLiteral and EnvelopePath. Exported from ".", "./browser" and
  "./procedures". The interpreter (hydrateInput) does not change: the
  envelope form converts to the $proc form.
- A call envelope gives { $proc, input }, and the header name gives $name.
  A ref into the world ({ type: "ref", value: ["world", ...path] }) is
  also a type. A value of null gives a call without input (the procedure
  gets undefined, and passThrough applies); a literal null gives null.
- ["names", "last"] gives $last, ["names", n, ...fields] gives "n.f1.f2"
  (each key a string without a dot or an integer >= 0). Another root
  (self, world, event, caps) is an error that names the root.
- { type: "literal", value } gives { $literal: value }. In an envelope
  program, an object with a $proc, $ref or $literal key, or a runtime
  ProcedureRef, is data: it goes into a $literal.
- No $when in the envelope form. toEnvelopeProgram refuses $when, other
  keys on a call or a $ref, a ref to a step named "last", and data that
  the envelope form reads in another way when it holds a construct. The
  round trip gives the same program, except a call with input null and
  such data, which come back in a $literal with the same values.
- Client.exec/execStream accept an envelope root: it converts first, then
  runs as before; exec(program, input) merges input as for a $proc root.
  Every other form and error message is unchanged.
- Tests: the PROGRAM-FORMAT examples in both forms (results, conversion
  both ways), escapes, errors, exec/execStream, define, and a fast-check
  property that the round trip hydrates to the same value.
- test/api/public-api.txt updated on purpose (UPDATE_API_SNAPSHOT=1): the
  new exports and the widened exec/execStream parameter.
- PROGRAM-FORMAT.md: new section 11 "The envelope form" (mapping table,
  example, rules); "Open questions" is now section 12, with the two
  in-document references and the pointer in deep-dive STATUS.md updated.
- Changeset: patch of the client group (additive, D13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mark1russell7
mark1russell7 merged commit 1d566a6 into main Oct 21, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant