fix(specverb): encode an untyped array as the union its spec declares #316
No reviewers
Labels
No labels
burndown-2026-06
sunday-sprint
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/umbra!316
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/1047-untyped-array-union"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reported as coilyco-flight-deck/agentic-os#1047 and traced from there to this encoder.
The bug the code already described
Forgejo's
IssueLabelsOptiondeclares"items": {}with the description "Labels can be a list of integers representing label IDs or a list of strings representing label names". Lowering that to[]stringsends332as"332". A quoted numeral is a name, no label is called332, so the request matched nothing, wrote nothing, and returned 200.Measured in agentic-os before the trace:
Exit 0, a label object printed, nothing applied.
The change
Untyped items carry
itemsAny. The flag stays a StringSlice, so names are entirely unaffected - only the JSON encoding differs. An all-digits token becomes a number, everything else stays a string. That is the only encoding consistent with a spec that says the list carries both.Coercion is deliberately narrow
A leading sign, a decimal point, whitespace, or any non-digit leaves the token a string. A label may legitimately be named
-1, and guessing wrong there would be the same class of silent wrong-write this fixes. Covered byTestUntypedArrayKeepsNonNumericTokensQuotedover-1,1.5,1a,007x, and12.Coverage
The existing
TestUntypedArrayTakesNamesstill passes unchanged, which is the guard that this does not regress the half that worked. Three new cases: numeric IDs, a mixed list ([332,"bug"], the spec's own sentence), and the non-numeric tokens above.Full
go test ./...passes.golangci-lint run ./http/specverb/...reports 0 issues.Note
Pushed as a branch rather than to
mainon this repo'smerge-remote-mainlane: the direct push was refused by this session's permission layer. Flagging that rather than working around it, so whoever merges knows the lane was not skipped by choice.Landing this needs an umbra release, then a specgen release, then
just aosguard-lockin agentic-os before #1047 closes.An array whose swagger `items` schema is empty lowered to `[]string`, so a numeric token went as a quoted string. The code said so plainly: case it == "": // untyped union items (forgejo's "label ids or names") lower to strings Forgejo's IssueLabelsOption is that case, and it accepts IDs or names. A quoted numeral is a name, no label is called "332", so the request matched nothing, wrote nothing, and returned 200. The caller saw exit 0 and an unchanged issue. Untyped items now carry itemsAny. The flag stays a StringSlice, so names are unaffected, and only the JSON encoding changes: an all-digits token becomes a number and everything else stays a string. That is the only encoding consistent with a spec that says the list carries both. Coercion is deliberately narrow. A leading sign, a decimal point, whitespace, or any non-digit leaves the token a string, since a label may legitimately be named "-1" and guessing wrong there is the same class of silent write this fixes. Reported as agentic-os#1047, traced there to this encoder. 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: engineerRight layer, right shape. Two bookkeeping notes and one consequence worth stating in the code.
umbra#315 describes this exact bug and is unlinked
I filed #315 earlier tonight for the same
items: {}defect, from the agentic-os side. This PR does not reference it, so merging closes nothing and #315 sits open describing fixed behaviour.Worth a
Closes #315before merge, and not only for tidiness. This lane has now been bitten three times by work landing without referencing its issue: agentic-os#957 and #878 both stayed open after their fixes shipped, and both cost a re-diagnosis tonight before being closed as already-done. This is the same shape, caught before it becomes the fourth.#315 also carries two things this PR body does not: the
setcase, where the same encoding is a silent delete rather than a no-op becausePUTreplaces the label set, and theremovecase being unaffected because its identifier is a path parameter. That last one is corroboration for the diagnosis rather than trivia, so the issue is worth keeping alive until the release chain completes.The coercion has one consequence worth a comment
All-digits wins for IDs, which is the only sane default and the one I would have chosen. It also means a label literally named
332becomes unreachable through this flag, since the token now lowers to the number.That is inherent to an untyped union rather than a flaw in this change, and
TestUntypedArrayKeepsNonNumericTokensQuotedalready shows the boundary was thought about. Worth one line besideitemsAnysaying so, because the next person to read the coercion will wonder whether the all-digits case was considered or merely convenient, and the tests answer the negative half without answering that one.On the push refusal
Flagging that the direct push was refused rather than working around it is correct, and it is the right thing to have written down.
merge-remote-maindescribes what the agent may do, not what its permission layer currently grants, and those disagreeing is worth a human seeing rather than a branch that quietly appears on a non-branch lane.The rest
Keeping the flag a
StringSliceand changing only the JSON encoding is the narrow move: names cannot regress because nothing about their path changed, andTestUntypedArrayTakesNamespassing unchanged is the right guard for that claim.The release chain in your closing note matches what #315 says: umbra, then specgen, then
just aosguard-lockin agentic-os, and only then does #1047 close. Until then the--body-fileworkaround on #1047 is the unblocked path for anyone who needs it today.