Expose every common entry field, and unbreak the Homebrew CI step - #73
Conversation
Two independent fixes.
## Spec 2.3a: common fields are not the operation's to decide
`getFields` walked the source operation's `entry` object and exposed the fields it
bound to a variable, so a payload's surface was whatever the CLI version that
generated the operation happened to write. Measured across the committed fixtures:
| field | template client | edge-case client |
| --- | --- | --- |
| posted | 9/9 | 1/5 |
| description | 0/9 | 1/5 |
| tags, groups, conditions | 0/9 | 0/5 |
So `tags`, `groups` and `conditions` were reachable from no payload at all, and
`description` from one. That is what spec 2.3a forbids in as many words: the
operation is a codegen input, never the transport, and a payload travels as an
`AddLedgerEntryInput`, so what the operation binds places no limit on what the
payload may carry. Deriving the set invents a restriction the API does not have and
moves a payload's surface whenever the CLI changes -- the spec 2.6 breakage the
whole design is meant to prevent.
`COMMON_ENTRY_FIELDS` now names the five, with the types `LedgerEntryInput`
declares, and `getFields` appends any the operation did not already bind. Appended
rather than interleaved, so the operation's own fields keep their source order.
`lines` stays excluded: it cannot be combined with an entry that has a `type`.
Two existing tests asserted the old rule and have been updated, and a new one
derives a payload from an operation binding none of the five and asserts all five
are still settable -- which is what proves they are not derived.
## Homebrew
`integration-tests` has been failing on every run since the formula moved:
==> Pouring ca-certificates--2026-07-16.all.bottle.2.tar.gz
##[error]unknown install step: run
You have disabled automatic updates and have not updated today.
The workflow caches `/home/linuxbrew/.linuxbrew` under a fixed key, so it restores
whatever Homebrew was current when that key was first written, and the runner
disables auto-update. A months-old Homebrew cannot pour a current bottle. Added
`brew update` before installing, which is Homebrew's own advice in that message,
and rotated the cache key so the poisoned cache cannot persist.
The step dies before any test runs, so this was never about the PR's code -- but it
does mean `addLedgerEntries commits a batch of typed payloads` has still never
executed anywhere.
The committed clients under `tests/fixtures/` are generated output, and the previous commit changed the generator without regenerating them. They were stale in a way that mattered: `batch-ledger-entries.test.ts` imports its builders from them, so those tests were exercising pre-fix code, and `template-schema-client.test.ts` reads them as text and passed only because of it. Regenerated with `scripts/update-test-schema.sh`'s codegen steps. Every payload now carries the five common fields: | field | template client | edge-case client | | --- | --- | --- | | posted | 9/9 | 5/5 | | description | 9/9 | 4/5 | | tags | 9/9 | 4/5 | | groups | 9/9 | 5/5 | | conditions | 9/9 | 5/5 | The 4/5 is `FixedValuesV1`, whose operation pins `description` and `tags` to literals. A field the operation fixes is still posted but stays out of the caller's hands, which is the same rule spec 2.3 applies to parameters — exposing it would let a caller override a value the operation deliberately set. Two tests asserted the behaviour this stack is removing, and both passed only against the stale fixtures: - `exposes the fields each operation set binds, and no others` asserted that the plain operation set's payload has no `tags`, `groups` or `conditions`. Those two operation sets come from different CLI generations, which is exactly the drift spec 2.3a exists to hide from callers, so the test now asserts the opposite: the same surface whichever generation produced it. - `binds different fields per entry type within one operation set` asserted `CardSettleV1` has no `conditions` because its operation does not bind them. Now it does, like its siblings. 74 passing; the 13 failures are the credential-gated integration tests, unchanged from before this work.
Possible non-conformance: pinned common fieldsFlagging a judgement call in this PR rather than leaving it to be discovered.
description: "posted by the nightly sweep"
tags: [{ key: "source", value: "sweep" }]That is why the edge-case client is 4/5 rather than 5/5 on those two fields. Every other payload in both fixtures is 5/5. Why I did it this way. §2.3 says a parameter bound to something other than a variable "MUST be skipped. The value is fixed by the operation and MUST NOT become a caller-supplied field." I applied the same reasoning to entry fields: the value is still posted, but exposing it would let a caller override something the operation deliberately set. Why it may be wrong. §2.3a is unconditional — "A payload MUST expose all of" — and lists the seven without carving out fields the operation pins. Read strictly, What settles it. One sentence in Worth noting the two readings differ in what a caller can actually do, not just in shape: under mine, an operation can guarantee an entry always carries a particular tag; under the strict reading, it cannot. Also still outstanding and not addressed here: node vendors none of the shared |
The runner's preinstalled Homebrew is months old and auto-update is disabled there, but formula definitions come fresh from the JSON API. A current ca-certificates bottle cannot be poured by that Homebrew -- it fails with `unknown install step: run` -- and the existing cache key restores the same stale Homebrew on every run. Bump the cache key and update Homebrew before installing. Ported from #73, which fixed this on another branch.
Stacked on #72. Two independent fixes, found by reviewing #72 against the shared spec with the fragment-ruby implementation as a lens.
1. §2.3a was inverted
getFieldswalked the source operation'sentryobject and exposed the fields it bound to a variable — so a payload's surface was whatever the CLI version that generated that operation happened to write. Measured across the committed fixtures, before the fix:posteddescriptiontags,groups,conditionstags,groupsandconditionswere reachable from no payload at all. The spec forbids this in as many words:That last clause was already true in this repo:
tests/template-schema/carries two operation sets from different CLI generations, and their payloads had different surfaces for the same entry type. A test asserted that as correct.COMMON_ENTRY_FIELDSnow names the five with the typesLedgerEntryInputdeclares, appended after the operation's own fields so source order is untouched.linesstays excluded — it cannot be combined with an entry that has atype.One deliberate exception: a field the operation pins to a literal stays out of the caller's hands.
FixedValuesV1pinsdescriptionandtags, so it is 4/5 above. It still posts them; exposing them would let a caller override a value the operation deliberately set, which is the rule §2.3 already applies to parameters.2.
integration-testshas been red on every runNot #72's fault — the step dies before any test runs:
The workflow caches
/home/linuxbrew/.linuxbrewunder a fixed key, so it restores whatever Homebrew was current when that key was first written, and the runner disables auto-update. A months-old Homebrew cannot pour a current bottle. Addedbrew update— Homebrew's own advice in that message — and rotated the cache key so the poisoned cache cannot persist.This is repo-wide, not PR-specific: it will block every node-client PR until fixed.
Tests
Three asserted the old rule and now assert the new one; the one that matters is new: derive a payload from an operation binding none of the five, and assert all five are still settable. That is what proves they are not derived.
The committed
tests/fixtures/generated-*-client.tsare regenerated — they are the actual generated output,batch-ledger-entries.test.tsimports its builders from them, and leaving them stale would have meant those tests exercising pre-fix code.74 passing. The 13 failures are the credential-gated integration tests, unchanged from before this work — including
addLedgerEntries commits a batch of typed payloads, which has never executed anywhere.Not included
No
x-fragment-experimentalheader. fragment-ruby foundaddLedgerEntriesis gated behind one, but per Steven that requirement is being removed, so node should not grow a flag it will immediately have to drop.Still outstanding from my review and not in this PR: node vendors none of the shared
spec/conformance/fixtures, which §5 makes a MUST. That is the remaining gap.🤖 Generated with Claude Code