A subject id is a string, whatever the row is keyed by - #139
Merged
Conversation
czpython
force-pushed
the
subject-id-is-a-string
branch
from
July 28, 2026 16:04
b76976d to
618631b
Compare
A subject id is a string everywhere it is read — the URL segment, the DBOS attribute, the dedup key — while the row behind one is keyed by an integer. Every summary bridged that itself with `id=str(row.id)`, and a constructor written for one field went on to hand-copy the rest. `SubjectId` takes either and is always the string, the way GraphQL's ID, JSON:API's id and ProtoJSON's int64 all settle it. With `from_attributes` on the header, a subject builds its own: the platform default is `model_validate(self)`, and a note's summary is its columns rather than a constructor listing them. What is left composes — a name reached through a relation, a URL, a title the row does not carry.
`WorkItemSummary` and `DashboardItem` each listed every field twice: once as a declaration and once as an assignment in a constructor. What the copying hid was the small amount of real work — a project name reached through a relation, and a set of links. The relation is a validation alias and the links are computed from the fields the shape already carries, so both build with `model_validate` and neither constructor survives. `Links` keeps the one that earns it: the URL rule, in a single place, called by both. Both responses serialize byte-for-byte as before; `ticket_url` rides the dashboard item only to feed its links, and stays off the wire.
czpython
force-pushed
the
subject-id-is-a-string
branch
from
July 28, 2026 16:06
618631b to
4a297b2
Compare
The dashboard item carried a `links` object no page ever opened — its one
consumer passes `prUrl={null}` — and the work item summary carried a
`ticketUrl` beside the `links.ticket` the UI actually reads. Both were the
same URL twice, or nobody's.
The dashboard item drops its links, the summary keeps the three the UI
opens and carries `ticket_url` only to build them, and the rule that
writes a GitHub URL moves to the one shape left that needs it.
czpython
enabled auto-merge (squash)
July 28, 2026 16:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A subject id is a string everywhere it is read — the URL segment, the DBOS attribute, the dedup key, the event row — while the row behind one is keyed by an integer. Nothing named that, so every summary bridged it by hand:
And a constructor written for that one field went on to hand-copy the rest, which is how
NoteSummary.from_noteended up listing five fields to transform one.The identifier is a type
Takes either and is always the string — the same settlement GraphQL, JSON:API and Protobuf all reached independently:
ID— "often numeric, it should always serialize as a String", and "any string (such as"4") or integer (such as4) input value will be accepted".id,type, andlidmembers MUST be strings."int64becomes a decimal string, "either numbers or strings are accepted", because JSON parsers are "silently lossy if a number is an integer larger than 2**53".Scoped to
idrather thanConfigDict(coerce_numbers_to_str=True), which would widen every string field on the model: alabelor atitlearriving as a number stays a mistake.What it removes
from_attributeson the header means a subject builds its own. The platform default on both bases is nowSubjectSummary.model_validate(self), and a note's summary is its columns rather than a constructor listing them —from_noteis gone, andfield_notes'schemas.pyno longer imports itsmodels.pyat all.What remains composes, and the next commit shows how little of it there was.
Not a wire change
id=str(item.id)andid=item.idserialize identically. No response shape moves, so the frontend is untouched.Ship's own shapes follow
WorkItemSummaryandDashboardItemeach declared every field and then assigned every field, which buried the only two bits of real work: a project name reached through a relation, and a set of links.The relation is an alias and the links compute from fields the shape already carries, so both build with
model_validateand neither constructor survives.Linkskeeps the one that earns it — the URL rule, in one place, called by both.Both serialize byte-for-byte as before.
ticket_urlridesDashboardItemonly to feed its links and stays off the wire viaexclude=True, andsource_idkeeps its name through a validation alias.ProjectRepoSummary.from_repois deliberately untouched: it runsrepo.get_status(), one database query per repo onGET /api/ship/projects— measured at 12 for 12 repos. Making it declarative would hide that, not fix it, and the fields it derives are the run state the platform now serves on the repo's own board. Its own ticket.