docs(specverb): state the mapped-body string limit, and refuse a shape that wants past it #320

Merged
coilyco-ops merged 1 commit from aos/claude/sb46-mapped-body-limit into main 2026-08-25 10:19:49 +00:00
Owner

Two of the three things #312 asked for, which the issue itself scopes as the cheap pair: "Items 1 and 2 are cheap and would have saved the entire investigation that produced this."

1. Say it in the docs

docs/specverb-request.md gains a section stating the limit and, more importantly, the part neither existing rule says on its own:

map is the only construct that renames an input to a different upstream key, because a body field carrying an upstream alias is refused outright. So a guardfile whose upstream needs a parameter name colliding with a reserved engine flag is forced into mapped-body mode for that leaf, and mapped-body mode then forbids every non-string value on that same leaf, including parameters unrelated to the collision that forced it.

An author reading either rule alone would not predict the combination. That is why this was worth writing down rather than left to the source.

The Exa measurement is carried over, because it is what makes the cost legible: absent is 200, contents={"text": true} is 200, contents="text" is HTTP 400, and that 400 is the first and only notice, arriving in production against a metered API.

2. Fail at parse time where the intent is visibly non-string

type=, format=, and a nested block are what an author reaches for when they want a non-string leaf. All three now fail closed with the limit named and a pointer to the issue that explains it:

`map` takes only `to="..."`, not type: a mapped leaf projects a string, so it
cannot declare a type or a nested shape (umbra#312)

Previously type="object" produced `map` needs exactly one `to="..."` property, which names nothing and reads as a syntax slip.

A missing to= keeps its own message. It is a different mistake, and naming the string limit there would be noise.

3. Typed mapped leaves: not done, and why

The issue scopes this as its own design pass, "probably not worth doing on the strength of one consumer", and that stands.

Worth recording one thing I checked and could not use. It would be better to validate the mapped target against the operation's declared type rather than refuse a hand-written shape, since the engine would then know rather than guess. That is not reachable here: validateBodyMappingMode refuses body fields and body mappings in combination, so a descriptor in mapped-body mode carries no declared type for the target at all. The check has nothing to check against.

So the refusal above is the strongest available signal short of item 3, not a stand-in for a check that was easier.

Boundary with #311

Untouched, as the issue asks. A seeded fixed value is written as its own JSON shape and never passes through mappedString, so pinned constants keep their types and this changes nothing about them.

Verification

make test, make vet, make lint (0 issues), and godoc-current.txt regenerated. Two new tests: the three shapes refuse with the limit named, and a missing target does not borrow that message.

Refs coilysiren/inbox#426

🤖 Generated with Claude Code

Two of the three things #312 asked for, which the issue itself scopes as the cheap pair: *"Items 1 and 2 are cheap and would have saved the entire investigation that produced this."* ## 1. Say it in the docs `docs/specverb-request.md` gains a section stating the limit and, more importantly, **the part neither existing rule says on its own**: `map` is the only construct that renames an input to a different upstream key, because a body field carrying an upstream alias is refused outright. So a guardfile whose upstream needs a parameter name colliding with a reserved engine flag is *forced* into mapped-body mode for that leaf, and mapped-body mode then forbids every non-string value on that same leaf, including parameters unrelated to the collision that forced it. An author reading either rule alone would not predict the combination. That is why this was worth writing down rather than left to the source. The Exa measurement is carried over, because it is what makes the cost legible: absent is 200, `contents={"text": true}` is 200, `contents="text"` is **HTTP 400**, and that 400 is the first and only notice, arriving in production against a metered API. ## 2. Fail at parse time where the intent is visibly non-string `type=`, `format=`, and a nested block are what an author reaches for when they want a non-string leaf. All three now fail closed **with the limit named** and a pointer to the issue that explains it: ``` `map` takes only `to="..."`, not type: a mapped leaf projects a string, so it cannot declare a type or a nested shape (umbra#312) ``` Previously `type="object"` produced `` `map` needs exactly one `to="..."` property ``, which names nothing and reads as a syntax slip. A missing `to=` keeps its own message. It is a different mistake, and naming the string limit there would be noise. ## 3. Typed mapped leaves: not done, and why The issue scopes this as its own design pass, *"probably not worth doing on the strength of one consumer"*, and that stands. Worth recording one thing I checked and could not use. It would be better to validate the mapped target against the operation's declared type rather than refuse a hand-written shape, since the engine would then know rather than guess. **That is not reachable here**: `validateBodyMappingMode` refuses body fields and body mappings in combination, so a descriptor in mapped-body mode carries no declared type for the target at all. The check has nothing to check against. So the refusal above is the strongest available signal short of item 3, not a stand-in for a check that was easier. ## Boundary with #311 Untouched, as the issue asks. A seeded fixed value is written as its own JSON shape and never passes through `mappedString`, so pinned constants keep their types and this changes nothing about them. ## Verification `make test`, `make vet`, `make lint` (0 issues), and `godoc-current.txt` regenerated. Two new tests: the three shapes refuse with the limit named, and a missing target does not borrow that message. Refs coilysiren/inbox#426 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(specverb): state the mapped-body string limit, and refuse a shape that wants past it
All checks were successful
ci / secrets (pull_request) Successful in 13s
ci / lint (pull_request) Successful in 45s
ci / test (pull_request) Successful in 1m0s
232675aae8
A `body` block written as `map` projects a string at every mapped leaf, whatever
the caller supplies, so an upstream requiring an object, number, or boolean at a
mapped parameter is unreachable through `map` in every configuration. Nothing
surfaced that. A guardfile mapping such a parameter parsed, built, shipped,
registered its tool, and then failed on every call with the upstream's own
error, which against Exa's `/search` was an HTTP 400 arriving in production
against a metered API.

Two of the three things umbra#312 asked for, which it scoped as the cheap pair.

The limit is now stated in docs/specverb-request.md, with the part neither rule
says on its own: `map` is the only construct that renames an input to a
different upstream key, so a guardfile whose upstream needs a name colliding
with a reserved engine flag is forced into mapped-body mode for that leaf, and
mapped-body mode then forbids every non-string value on that same leaf,
including parameters unrelated to the collision that forced it.

Writing a shape onto a `map` now fails at parse time with the limit named.
`type=`, `format=`, and a nested block are what an author reaches for when they
want a non-string leaf, and they used to produce a generic property-count
message that named nothing. A missing `to=` keeps its own message, because it is
a different mistake and naming the limit there would be noise.

Not done, and unchanged: typed mapped leaves. BodyMapping would need a declared
type and projectMappedBody a typed accessor, and the issue scopes that as its
own design pass. The engine also cannot check the target against the spec here,
because mapped-body mode refuses body fields outright, so no declared type for
the target exists at validation time.

Refs coilyco-flight-deck/umbra#312, coilysiren/inbox#426

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: platform
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/umbra!320
No description provided.