Skip to content

docs(emitters): make plan.Policy data and define its envelopes - #1331

Open
OmarAlJarrah wants to merge 5 commits into
docs/emitters-inclusive-unionsfrom
docs/emitters-envelopes-policy-data
Open

OmarAlJarrah wants to merge 5 commits into
docs/emitters-inclusive-unionsfrom
docs/emitters-envelopes-policy-data

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Two gaps in plan.Policy, resolved as one contract, because the shape of the envelope policy depends on what the policy may hold.

The policy is data (#1014). plan.Policy held a function, PrimaryResponse, and Plan.Version hashed the policy. A function has no stable bytes. encoding/json/v2 refuses the field, its printed form is an address that changes every run, and leaving it out gives two policies that pick different responses the same version, so the plan cache would serve one the other's plan. Now:

  • every field is a value from a closed vocabulary the plan package defines. PrimaryResponse is an ordered list of rules (lowest_2xx, unconditional, default), and the first one an operation meets picks. The Default's other use, the catch-all failure, is its own switch, DefaultFails. The defaults reproduce today's behaviour exactly, and "neither or both" stays expressible.
  • Version hashes the document's digest (canonical JSON with sources left out, which Build never reads), a new plan.Revision, and the policy's deterministic JSON.
  • plan.Revision is a departure from the issue, which hashed only the document and the policy. Without it, a fix to Build would leave every cached plan current. The plan's goldens hold it the way IRVersion holds the IR's shape: a golden that changes at an unmoved Revision fails.
  • A heuristic registered from outside by name and version was weighed and refused, and the document says why. Changing its code without its version is a silent cache hit, which is the failure this removes. A caller-supplied policy ID fails the same way, so a new heuristic joins the vocabulary at a new Revision.

Envelopes (#695). EnvelopeDetect EnvelopePolicy named a type nothing defined. plan.Policy.Envelopes now lists the envelopes a configuration names, and the default names none:

  • Matching. A body matches by the Bases its model reaches through Base/Mixins, or by the Members it declares. The issue proposed composition alone, but on the golden spec that misses about one envelope in six and needs 113 IDs (numbers below). Members must name every member, so a body that merely has a result is never unwrapped. A per-operation opt-out is Except []ir.OpID in the policy rather than a hint as the issue suggested: the plan reads no hint (the plan-or-refine test's second rule).
  • Reading (ResponsePlan.Envelope, docs/emitters/plan/responses.md#envelopes). A matched success returns the member at Value. A pager's page is read whole, its items where Pagination.Items says. The rest of the envelope stays reachable: an enveloped operation has a WithResponse method, whose Response carries Envelope.
  • In-band failures. The flag is read before the body, as a discriminator's tag is. A 2xx whose flag holds Fails is a failure with no fault and no kind, retried only as its status would be. Its body decodes as the first error entry the same envelope matches, and is never decoded as the success type: 2,200 golden success bodies declare success: enum [true], which a failing body cannot decode into.
  • Failures are read whole. The docs emitter documents both the returned type and the wire body, and the mock sends the whole envelope.

The page-information output the issue asked for is not added. The pager already reads its paths from the IR's Pagination, and the envelope stays reachable for everything else.

Measured on the golden spec (spec @319dc756, morphic @b206dc99), primary body = lowest 2xx, JSON content:

  • 2,665 of 3,763 operations' primary bodies declare success, errors, messages and result.
  • 2,238 of them reach a named component declaring success through Base/Mixins (113 distinct components). 427 declare it inline.
  • 2,321 of the 2,665 declare an error case the envelope matches, so their in-band failure is typed. 2,225 of those have exactly one such type.

Out of scope: hashing naming.Policy and the runtime policy, which the design never hashes. What the manifest records about them is #713's.

Closes #1014
Closes #695

Test plan

  • Envelope counts measured by a script over the golden IR, following Base/Mixins chains and flattened members. An earlier reading of "a component declaring all four members" gave 667 and was wrong: the shared components declare success, errors and messages, and the body adds result. The text states the corrected measure.
  • The Policy, Envelope and example literal, extracted verbatim against stub ir types, pass go vet.
  • Every doc naming EnvelopeDetect, EnvelopePolicy or the function-valued PrimaryResponse swept: policy.md was the only one. prior-art.md's "unwrap decisions are recorded as PropPaths" still holds.
  • CI: TestDocuments_LinksResolve, TestEmitterDocs_TargetStepsAreCanonical and the other doc tests over the new anchors and step row.

Stacked on #1329, because both edit docs-and-mock.md, refine/steps.md and the end of docs/emitters/testing/mandatory-tests.md. Merge #1329 first. Once it lands, GitHub retargets this PR to main, and "Update branch" (a merge of main, which conflicts nowhere) brings the diff back to this change alone.

plan.Policy held a function, PrimaryResponse, and Plan.Version hashed the
policy. A function has no stable bytes: json/v2 refuses it, its printed
form is an address that changes every run, and skipping it gives two
policies that pick different responses one version. The policy is now
data from a closed vocabulary: PrimaryResponse is an ordered list of
rules and DefaultFails says whether an unpicked Default is the catch-all
failure. Version hashes the document's digest without sources, a new
plan.Revision bumped with every change to Build's output, and the
policy's deterministic JSON.

EnvelopeDetect named a type nothing defined. plan.Policy.Envelopes now
lists envelopes a configuration names, none by default. Each matches a
body by the bases it reaches or by the members it declares, and says
where the value sits and which flag value means the call failed. A
matched success returns that member, keeps the envelope reachable
through WithResponse, and turns a 2xx whose flag says it failed into a
failure with no fault or kind, decoded as the operation's matching error
body. Failures and pages are read whole.

T-30 pins the plan's identity and T-31 the envelope reads.
…into docs/emitters-envelopes-policy-data

# Conflicts:
#	docs/emitters/docs-and-mock.md
#	docs/emitters/testing/mandatory-tests.md
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.

docs/emitters: the plan's version hashes a policy whose functions have no stable bytes docs/emitters: EnvelopePolicy is named but never defined

1 participant