Plain generated reads return a person's string id as a string in Ruby, Python and TypeScript - #917
Conversation
There was a problem hiding this comment.
🟢 Approved
The marker-derived tables agree across all three SDKs, all response paths are covered, and no unresolved correctness issues were found.
Pull request overview
Aligns Ruby, Python, and TypeScript person-ID decoding with Go’s FlexibleInt64 behavior using generated, operation-specific response paths.
Changes:
- Generates consistent person-ID site tables from
x-go-type. - Applies decoding to single, mutation, paginated, and wrapped responses.
- Adds cross-SDK tests and documents the decoding contract.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
SPEC.md |
Defines typed person-ID decoding. |
typescript/package.json |
Adds site-table generation. |
typescript/scripts/extract-person-id-sites.ts |
Generates TypeScript response paths. |
typescript/src/generated/person-id-sites.ts |
Contains generated TypeScript sites. |
typescript/src/services/base.ts |
Decodes IDs across response paths. |
typescript/tests/services/person-id-decode.test.ts |
Tests typed decoding comprehensively. |
typescript/tests/services/person-id-normalization.test.ts |
Updates normalization expectations. |
typescript/tests/services/my-notifications.test.ts |
Tests range refusal. |
typescript/tests/services/schedules.test.ts |
Updates read-failure expectations. |
typescript/tests/services/todos.test.ts |
Updates composite read expectations. |
ruby/scripts/generate-metadata.rb |
Generates Ruby response sites. |
ruby/scripts/generate-services.rb |
Passes operation IDs to decoding. |
ruby/lib/basecamp/generated/metadata.json |
Stores generated Ruby sites. |
ruby/lib/basecamp/person_id_sites.rb |
Implements Ruby typed decoding. |
ruby/lib/basecamp/http.rb |
Decodes single and paginated bodies. |
ruby/test/basecamp/person_id_sites_test.rb |
Tests Ruby decoding paths. |
ruby/test/basecamp/services/schedules_service_test.rb |
Updates schedule expectations. |
ruby/test/basecamp/services/todos_service_test.rb |
Updates todo expectations. |
ruby/lib/basecamp/generated/services/account_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/attachments_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/automation_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/bookmarks_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/boosts_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/calendars_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/campfires_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/card_columns_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/card_steps_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/card_tables_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/cards_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/checkins_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/client_approvals_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/client_correspondences_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/client_replies_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/client_visibility_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/cloud_files_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/comments_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/documents_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/event_feed_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/everything_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/folders_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/forwards_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/gauges_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/google_documents_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/hill_charts_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/message_boards_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/message_types_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/messages_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/my_assignments_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/my_notes_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/my_notifications_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/people_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/projects_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/recordings_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/reports_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/schedules_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/search_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/subscriptions_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/templates_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/timesheets_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/todolist_groups_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/todolists_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/todos_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/todosets_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/tools_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/uploads_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/vaults_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/webhooks_service.rb |
Supplies decode operation IDs. |
ruby/lib/basecamp/generated/services/wormholes_service.rb |
Supplies decode operation IDs. |
python/scripts/generate_services.py |
Generates Python response sites. |
python/src/basecamp/_person_id.py |
Implements Python typed decoding. |
python/src/basecamp/generated/services/_person_id_sites.py |
Contains generated Python sites. |
python/src/basecamp/generated/services/_base.py |
Applies synchronous decoding. |
python/src/basecamp/generated/services/_async_base.py |
Applies asynchronous decoding. |
python/tests/test_person_id.py |
Updates normalization behavior tests. |
python/tests/test_person_id_sites.py |
Tests Python decoding comprehensively. |
python/tests/services/test_notifications.py |
Tests notification range refusal. |
python/tests/services/test_todos.py |
Updates composite read expectations. |
Review details
- Files reviewed: 22/77 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Ruby, Python and TypeScript handed an untagged person's string id ("7")
back as the string on a plain generated read, where Go's decoder reads 7
because generated Person.Id is types.FlexibleInt64. Each generator now
emits, per operation, the response sites whose schema reaches Person,
selected on the x-go-type marker rather than on key names, and the
generated read path decodes the id there as FlexibleInt64 after the
normalizer: value converts, syntax refusal reads 0, range refusal and
non-int64 values fail the read. Plain-int64 people (upcoming schedule,
my assignments, out of office, template-library confirmation) are not
sites and stay strict.
Go trims a followed page to the list cap before decoding its items, reads only the events of a later GetPersonProgress page, and decodes later pages of gauges, needles and bubble-ups into hand-written types whose plain int64 id reads null as 0. The ports decoded every followed page whole, so each of those reads failed where Go and main read them.
2935fea to
96bf69c
Compare
|
🤖 Adversarial review. A separate Claude Opus agent reviewed this PR, not Codex and not a human. It was briefed to find a strict Round 1, at 2935fea: three findings. In each, Go and
Round 2, at 96bf69c: nothing real remains.
Two differences remain, and both are declared rather than fixed:
|
|
Ran an adversarial pass over this at the head (96bf69c). No merge blocker — the parts that would be expensive to get wrong are right. Three comments, none of which I think should hold the merge. What checked out, so the review is legible rather than just a list of doubts: all three site tables contain exactly the same 161 operations and 359 sites, and an independent traversal of the generated Go types matched that set. Ruby and Python reproduce 1. Site discovery can silently lose coverage when the schema changes. All three walkers stop at a recursive reference without checking whether the deeper instance contains people — 2. The TypeScript float divergence is real but waiver 1B.6 doesn't cover it. 3. TypeScript preserves numeric negative zero. |
In Ruby, Python and TypeScript, a plain generated read returned a person's string id as a string. If the person had no
personable_typeand its id came as"7",comments.getgave back"7". In TypeScript that string sat in a field typednumber. Go gives7, because its generatedPerson.Idistypes.FlexibleInt64and every Go service decodes through the generated types first.Tracked in Plain generated reads leave an untagged person's string id unconverted in Ruby, Python and TypeScript.
The obvious fix is to widen the normalizer, and it is wrong. The keys it would match (
creator,assignees,person, …) also hold people whose id is a plainint64in Go:UpcomingSchedulePerson,MyAssignmentAssignee,OutOfOfficePersonandTemplateLibraryConfirmationPerson. A string at any of those is a decode error in Go. That is why Seven SDKs read a person id seven ways, and Go disagreed with itself narrowed the walk. Merge-safe writes refuse assignee ids the API really sends then closed the write path, which left the plain read.So the fix goes where Go's fix is: typed decode, at exactly the fields Go reads as
FlexibleInt64.Audit.
FlexibleInt64is on exactly one generated field,Person.id. No hand-written Go wrapper uses it. Every Go service for the affected operations runs the generatedParse<Op>Responsebefore anything else. On the gauge and notification surfaces, the positional normalizer covers the only person sites those operations have, so the result is the same. In total 161 operations reachPersonin a 2xx response, at 359 sites.Table. Each port's generator walks the OpenAPI response schemas and selects on the
x-go-typemarker, never on a key name. It emits a per-operation table of those sites, so the drift checks guard it.Decode. The generated read path applies the table after the normalizer, on every body it decodes: single reads, lists, and every page of paginated and wrapped listings, followed pages included. Ruby's generated services now pass the operation id to the body decode; before, mutations passed none. At a site, a person's
idreads asFlexibleInt64does:0;null, a boolean, an array or an object fails the read.A null or non-object person, a missing
id, and a malformed container are left alone. Go zero-fills or refuses those as part of whole-body decode, and these ports do that for no field.Followed pages. A followed page is decoded only as far as Go decodes it, and that is less than the first page:
GetPersonProgresspage decodes only itsevents.nullid, because Go decodes those pages into hand-written types with a plainint64id.An adversarial review found each of these as a read Go accepts and this branch refused.
Behaviour change. A person id Go refuses now fails the generated read. That includes the reads the merge-safe composites make before writing, so those composites now refuse at the read, as Go does. No write is sent either way.
SPEC. SPEC §10 gains "Person Ids at Typed Decode", and the "plain read is still open" note is gone.
Audit
The measurement: the card-42 id-shape corpus at every site, 13,239 cases, first run through Go's real
Parse<Op>Response(the oracle), then through each port's response path, onmainand on this branch. Each cell shows divergences from Go as rows Go reads as a value / rows Go refuses, main → branch. Every site's Go type is the one fieldPerson.id.creatortypes.FlexibleInt64assigneestypes.FlexibleInt64completertypes.FlexibleInt64completion_subscriberstypes.FlexibleInt64subscriberstypes.FlexibleInt64participantstypes.FlexibleInt64persontypes.FlexibleInt64GetPerson,ListPeople, …)types.FlexibleInt64boostertypes.FlexibleInt64performed_bytypes.FlexibleInt64grantedtypes.FlexibleInt64revokedtypes.FlexibleInt64approvertypes.FlexibleInt64UpcomingSchedulePerson,MyAssignmentAssignee,OutOfOfficePerson,TemplateLibraryConfirmationPersonpeopleint64Every remaining TypeScript divergence comes from the JavaScript number limit (waiver 1B.6). That is 4 corpus rows and 3 corpus rows at every site:
JSON.parsehas already rounded it if it arrived as a number.1024.0,1e3and the number 2^63, butJSON.parsehas turned each into an integer before any code sees it.In all three ports the 1,805 null, absent, non-object and container-shape cases are unchanged.
Per port, each new test was run with its runtime change reverted. The tests went red for the right reason:
'7'against7, no error raised, followed pages not decoded, and a key-name sweep breaking the strict-site tests. They went green again once the change was restored.make ts-check py-check rb-checkandconformance-{typescript,python,ruby}pass locally. Kotlin, Swift and Rust are not touched here; this is rebased on Swift and Rust refuse person shapes the reference writes back, and SPEC §10 keeps both sides.