| title | API Control Plane |
|---|---|
| description | Current-state architecture for Gas City's CLI, HTTP, SSE, generated client, and typed-wire contract. |
Last verified against code: 2026-04-22
This architecture doc captures the API control-plane invariants Gas
City has converged on. It is normative current-state documentation:
future contributions that violate these invariants are wrong unless a
conscious decision updates this document. Plans in plans/archive/
describe the journeys that produced these invariants; this document
describes the destination.
Two architectural themes run through everything below:
- The object model is the center; the CLI and the HTTP + SSE API are projections over it. One canonical domain, two typed surfaces.
- Typed data end-to-end. Go structs with annotations drive a generated OpenAPI 3.1 contract; every wire-visible shape appears in the OpenAPI spec; consumers in any language code against the same contract. Zero opacity on the wire.
internal/{beads, mail, convoy, formula, agent, events, session, sling, graphroute, agentutil, pathutil, cityinit, ...} is the
canonical domain. All business logic lives there. The two surfaces
below call into it; neither re-implements validation, routing, or
invariants.
City initialization is a worked example: the HTTP handler for
POST /v0/city does not shell out to gc init; it calls
cityinit.Service.Scaffold in-process, and the CLI drives the same
cityinit.Service.Init contract. The scaffolded city registers with
the supervisor synchronously before 202 Accepted returns; the
reconciler runs the slow finalize later and publishes a
request.result event. Both projections live on the same typed
contract and error sentinels (cityinit.ErrAlreadyInitialized,
ErrInvalidProvider, ErrMissingDependency, ErrProviderNotReady,
ErrInvalidBootstrapProfile). Long-running mutations in general
follow this shape: validate and create intent synchronously, return
202 with a request_id, run the expensive work in a background
goroutine, publish a request.result event on completion or
failure — subscribers watch the event stream instead of polling.
See engdocs/design/async-request-result.md for the full pattern.
cmd/gc/cmd_*.go internal/api/handler_*.go
(arg parsing, (Huma input/output types,
text formatting, handler bodies,
exit codes) typed error returns)
\ /
\ /
v v
internal/sling/ internal/convoy/
internal/agentutil/ internal/graphroute/
internal/pathutil/
|
v
internal/{beads, config, formula, molecule, agent, events, ...}
- Domain code has no I/O surfacing. No
fmt.Fprintf, noio.Writerparameters, no HTTP responses. Domain functions return values and errors. Text formatting is a CLI concern; JSON shaping is an API concern. - Narrow interfaces over flag bags. Domain-side dependencies
use focused interfaces (
AgentResolver,BeadRouter,Notifier,BranchResolver) validated at construction. - Intent-based APIs. Callers express intent (
RouteBead,LaunchFormula,AttachFormula,ExpandConvoy); implementation decides how (shell command, direct store, API call). No god-struct option bags passed around. - No upward dependencies. A lower layer never imports from a higher layer.
The CLI calls the core library directly. It is not a generic remote client; it coexists with a local supervisor in the same city by routing through HTTP only when lock coordination requires it.
Concretely, cmd/gc/apiroute.go:apiClient() implements this rule:
- No running local supervisor → CLI calls the core library directly against the on-disk stores.
- Running local supervisor with mutations allowed → CLI routes the mutation through the local HTTP API via the generated Go client. The supervisor executes the mutation under its own locks; the CLI's result is consistent with the supervisor's state.
Remote access is not the first-class reason this path exists. A
--base-url http://remote:port invocation is a side effect of the
same mechanism, not its purpose. The generated client is "library
calls dispatched over HTTP when we have to cross a process
boundary we didn't create."
Every HTTP + SSE endpoint is registered through Huma against annotated Go types. Huma generates the OpenAPI 3.1 spec from those types; the spec drives everything downstream.
Per-city routes are available only after the supervisor resolver
returns a running State for that city. During supervisor startup a
city may appear in GET /v0/cities with running=false and a startup
status such as starting_agents; /v0/city/{cityName}/... requests in
that window return 404 with the typed not-found problem detail. The
city list and lifecycle events are the readiness boundary for clients
that need to issue per-city requests.
internal/api/genclient/ has three in-tree consumer categories,
governed by a structural rule: direct consumption is allowed for
endpoints that (a) do not participate in write-side fallback (no
ShouldFallback path) and (b) do not require domain-type conversion
at the adapter seam. Anything that fails either test goes through
internal/api/client.go.
- CLI mutation coordination via
internal/api/client.go, used bycmd/gc/apiroute.goas described above. This is the only consumer for paths that mutate state and could race an in-process supervisor, or that need domain-type conversion (e.g. typedsession.SubmitIntentfrom a string wire field). The adapter also owns local-file fallback when the controller isn't running. - Read/stream CLI surfaces that import genclient directly —
currently
cmd/gc/cmd_events.go, which calls typed methods for event listing and SSE following. Events have no write-side fallback (no bus without a controller) and need no domain-type conversion, so they satisfy the structural rule. Future read-only CLI surfaces that meet the same two conditions are allowed to import genclient directly; no case-by-case approval needed. - Layer 2 conformance probe —
genclient_roundtrip_test.goexercises every generated method against a real supervisor so spec/reality drift fails CI.
The generated client is not promoted as a public Go SDK for
external consumers. External Go consumers, if they ever appear,
get a supported surface at that point; until then the internal/
location is load-bearing.
The dashboard is a static TypeScript SPA served by a tiny Go
binary (cmd/gc/dashboard/) whose only jobs are to embed the
compiled bundle and inject the supervisor URL into index.html.
The SPA talks directly to the supervisor's typed OpenAPI endpoints
from the browser — the dashboard server is NOT an API proxy. The
dashboard server also hosts one narrow operational debug endpoint
(/__client-log) that accepts browser error logs for centralized
debugging; this endpoint is intentionally outside the typed HTTP +
SSE control plane and may use standard encoding/json for body
decoding.
The invariants below apply to every operation under internal/api/
except the /svc/* workspace-service proxy (see §5).
Each endpoint is a Go function whose signature (typed input struct,
typed output struct) plus a huma.Operation value IS the endpoint
definition. Huma binds it, validates it, routes it, serializes it,
schema-describes it. There is no second description of the endpoint
anywhere — not in a router table, not in an OpenAPI YAML, not in a
client stub.
Framework-level cross-cutting wire contract (CSRF header, request-ID
response header, per-stream status headers) does not live on
per-endpoint struct annotations; it lives on the registration
helpers (cityPost, cityRegister, registerSSE) or on a
post-registration spec walker (registerFrameworkHeaders). That is
not a second description — it is the same mechanism applied
at one layer up, and the OpenAPI spec that results still describes
every operation's full contract. See §3.5.2. Patterns and Huma
quirks that inform these helpers are documented in
Huma Usage Notes.
internal/api/openapi.json and docs/schema/openapi.json are
outputs of cmd/genspec, which reads the live Huma registration
from a SupervisorMux. The pre-commit hook regenerates both on
every Go-file commit. TestOpenAPISpecInSync fails CI if the
committed spec drifts from what the supervisor serves.
Per-city operations live at /v0/city/{cityName}/....
Supervisor-scope operations live at their top-level paths. No
shadow mapping. No prefix-strip-and-forward. No client-side
path-rewrite helpers. The existence of such a helper is direct
evidence the spec disagrees with reality and is a bug to fix.
Every wire byte that represents a domain value comes from encoding a typed Go struct (schema-registered with Huma) through the standard JSON encoder, directly or via Huma's own serialization machinery. This principle forbids three anti-patterns specifically:
json.Marshal(map[string]any{...})— untyped input.fmt.Sprintf-built JSON strings — hand-constructed shape.json.Marshal(anyInterfaceValue)where the interface carries values whose types are not schema-registered — hides the shape from the spec.
The test a reviewer applies: is there any line in your code that
produces JSON-shaped output from non-typed or map-typed input? If
yes, violation. If every JSON byte comes from encoder.Encode of a
typed, schema-registered struct, the principle holds.
Protocol framing around domain data — HTTP status codes, HTTP
response headers, SSE id: / event: / data: / retry line
separators, chunked-encoding bytes — is not domain data and is not
in scope for this principle. The carve-out is direction-symmetric
and covers two specific files: internal/api/sse.go (emitter)
hand-writes the SSE protocol-text lines around a typed
encoder.Encode(data) call on a registered struct, and
cmd/gc/cmd_events.go:sseDecoder (consumer) hand-parses the same
SSE protocol-text lines and json.Unmarshals the data: payload
into typed genclient.* structs. In both directions the domain
payload IS framework-encoded/decoded; the surrounding protocol
literals are not JSON at all.
New SSE endpoints must register through registerSSE /
registerSSEStringID; ad-hoc SSE handlers outside those helpers
are not covered by this carve-out.
Edge cases that are NOT wire and therefore exempt:
- SQL/BLOB (de)serialization in storage packages.
- Hashing request bodies for idempotency keys.
- Parsing stored JSONL transcript/log files from disk.
- Parsing external-tool output we don't own (provider CLI stdout,
provider auth files like
~/.codex/auth.json). - Internal event-bus
[]bytepayloads between in-process emitters and consumers (these become typed at the wire via the registry — see §4).
Custom MarshalJSON / UnmarshalJSON on wire types are forbidden
with two narrow, documented exceptions:
SessionRawMessageFrame(internal/api/session_frame_types.go) — the raw-frame pass-through for provider-native session transcripts; forwards arbitrary JSON the provider wrote. See §3.6.EventPayloadUnion(internal/api/convoy_event_stream.go) — the wire wrapper aroundevents.Payloadthat emits the typed payload as a namedoneOfcomponent. ItsMarshalJSONemits the concrete variant directly (so the wire sees{"rig":...}rather than a wrapper object); its Schema method registers and refs the named component. Required to get a single namedEventPayloadcomponent schema that Go and TS clients can both consume.
Every response field, every SSE event payload, every input body is
a named Go struct with real fields and Huma tags. No
json.RawMessage or map[string]any in the typed control plane,
with exactly one class of exception (§3.6).
"Heterogeneous", "opaque", "clients render it generically", "we'll figure out the union later", and "it's just internal" are not qualifying exceptions. If our code constructs the map, we know the keys. Make it a struct.
3.5.1 No hidden inputs — every accepted parameter appears in the spec
Every input a handler reads MUST be a typed field on its Huma
input struct (path:, query:, header:, or Body). The
generated OpenAPI spec is the complete and exhaustive description
of the inputs an endpoint accepts. Running a request through a
handler must not produce a different outcome than running the same
request through the spec.
Three anti-patterns are specifically forbidden:
-
Dynamic or wildcard query parameters. Any scheme where a handler accepts query keys matching a pattern (
var.*,meta_*,x-*) rather than declared names. OpenAPI 3.1 cannot express wildcard query keys; accepting them creates a hidden contract the spec cannot describe. When a handler needs an open-ended string-to-string dictionary as input, move the input into a typed request body field (Vars map[string]stringon a POST body). Dictionary bodies have a schema; dictionary query parameters do not. -
Resolvers that read raw URL query or header values that aren't declared input fields.
huma.Resolverimplementations may validate or normalize values the struct already declares, but may not read keys offctx.URL().Query()orctx.Header()that aren't present on the input struct. If a resolver needs a value, that value is a declared field — no exceptions. -
Presence-vs-empty semantics via raw-URL inspection. If a handler behaves differently for "parameter absent" vs "parameter present with empty value", the presence flag must come from Huma's parameter binder — not from peeking at
ctx.URL().Query()inside a Resolver. Use thehuma.OptionalParam[T]-style wrapper (seeinternal/api/huma_optional_param.go): a custom type withSchema(),Receiver(), andOnParamSet(isSet bool, ...)that emits the underlyingT's schema on the wire and exposes anIsSetflag to the handler. Huma v2 does not support pointer query parameters (they panic at registration, seegithub.qkg1.top/danielgtaylor/humaissue #288);OptionalParamis the framework-sanctioned idiom.Practical corollary: Huma's parameter binder treats
?cursor=(empty value) identically to an absent parameter (huma.go:881-882: isSet = value != ""). A three-state contract (absent / present-empty / present-nonempty) is therefore not expressible against Huma; the wire contract collapses to two states. Design APIs around that.
The test a reviewer applies: does running an undeclared query parameter or an undeclared body field through the handler change its behavior? If yes, violation. The spec is the contract; the handler does not get a second, private contract the spec doesn't know about.
Huma does not reject undeclared query parameters by default (they are silently ignored). That is not permission to rely on them — silent acceptance of undeclared parameters is a property of the framework, not a blessing of hidden contract. Callers that send undeclared parameters are sending noise; handlers that read them are violating this principle.
Wire contract that applies uniformly across every operation — CSRF request headers, request-ID response headers, the custom response headers SSE streams emit for runtime status — is still real wire contract and must appear in the spec. OpenAPI 3.1 has no mechanism to declare headers "globally" for all operations (see speakeasy.com/openapi/responses/headers); the canonical pattern is:
- Define the header once. Request headers live as operation
parameters; response headers get a named entry in
components.headers. - Reference it from every operation it applies to (request
params go on the Operation's
Parameters; response headers use{"$ref": "#/components/headers/NAME"}).
Rather than embedding the reference in 50+ input/output structs, attach it at the single function every operation already flows through:
- Request headers (e.g.
X-GC-Request) —cityPost,cityPut,cityPatch,cityDelete, andcityRegisterininternal/api/city_scope.gopassaddMutationCSRFParamas a Huma operation handler. One line at the route helper covers every current and future mutation endpoint. - Response headers (e.g.
X-GC-Request-Id) —registerFrameworkHeadersininternal/api/huma_spec_framework.goruns once after all routes are registered. It populatescomponents.headersand walks every operation's responses to inject a$refpointing at the named component. - Per-stream custom response headers (e.g.
GC-Agent-Status,GC-Session-State,GC-Session-Status) — catalogued insseStatusHeaders(internal/api/sse.go) and referenced by name at eachregisterSSEcall site viasseResponseHeaders("GC-Agent-Status"). Colocated with the operation where the handler emits the header, one catalog entry per header.
These patterns are not exceptions to §3.5.1; they are the §3.5.1-compliant mechanism for cross-cutting concerns. The spec still fully describes the contract — every operation's parameters and response headers list the header explicitly — but the declaration happens at one function call, not fifty struct definitions. Middleware remains the single source of enforcement; the spec remains the single source of description; the helpers keep the two aligned.
Session transcript streaming and query endpoints forward
provider-native frames with full fidelity. Each response/envelope
identifies the producing provider via a provider field whose
value is one of the known provider keys (claude, codex,
gemini, open-code, etc.); each frame's JSON is emitted verbatim
as the provider wrote it, with no GC-side interpretation.
Consumers parse frames using provider-specific logic on their side,
keyed by the provider identifier on the envelope.
The single JSON-pass-through wire type is SessionRawMessageFrame
(internal/api/session_frame_types.go). Its Schema method emits
an "any JSON value" schema because Gas City does not own the
shape of provider frames. Publishing typed wire schemas for
provider frames would claim a contract we don't own: a provider
could change its frame shape tomorrow and the spec would silently
lie until regenerated. Honest opacity with a provider discriminator
is the right design.
Passing through externally-authored shapes is not a license to also opacify our own shapes that happen to be nested near them. Every GC-owned field on the same envelope as the raw frames (envelope metadata, provider identifier, session info) stays typed.
See §4.
Every error returned by a Huma handler is a
huma.StatusError-producing call with a real problem-details
body. No apiError{} shortcuts. No hand-written writeError.
For the outermost panic-recovery middleware (which must run before
Huma enters the stack), error bodies are pre-serialized
application/problem+json byte constants — one var declaration
per well-known error, no runtime json.Marshal. The constants
live in internal/api/middleware.go as problemBody values.
/svc/* is a raw pass-through to external service processes that
own their own contracts. It is explicitly not a typed API
surface. This is the single carved-out path inside internal/api/.
If /svc/* ever becomes typed, it gets its own migration.
Events are a first-class part of the typed wire contract. Both the
SSE streams (/v0/events/stream,
/v0/city/{cityName}/events/stream) and the list endpoints
(GET /v0/events, GET /v0/city/{cityName}/events) describe their
payload field as a named oneOf union covering every registered
events.Payload shape. There is no opaque payload: {} anywhere
on the wire.
- Bus layer (
internal/events) stores payloads as[]byteso it stays domain-agnostic.events.Eventandevents.TaggedEventare bus-internal types only; they are never returned directly from an HTTP handler. - Registry (
internal/events/payload.go) holds the event-type → Go-type mapping.events.RegisterPayload(typeConst, sample)associates a constant with a sample value of a type implementing the sealedevents.Payloadinterface.events.DecodePayloadturns bus bytes back into the registered typed value. - Emitters take values of
events.Payloadrather thanmap[string]any. The sealed interface keeps ad-hoc shapes out of emission sites at compile time. - Wire projection — the API-layer
WireEvent/WireTaggedEventtypes (list) andeventStreamEnvelope/taggedEventStreamEnvelope(SSE) carry a typedPayloadfield wrapped inEventPayloadUnion.EventPayloadUnion.Schemaregisters a namedEventPayloadcomponent whose schema is aoneOfof every registered payload type.
Every constant in events.KnownEventTypes MUST have a registered
payload. Events that carry no structured data register
events.NoPayload — a typed empty struct that still produces a
named schema variant so the wire stays uniform across event types.
TestEveryKnownEventTypeHasRegisteredPayload fails CI if a new
constant is added without registration; that's how the registry
discipline stays load-bearing rather than best-effort.
Decode-failure policy (uniform across list and stream). Decode
failures and unregistered event types are omitted from list and
stream output and logged via log.Printf; the wire never carries
a degraded envelope with nil payload. A malformed event is a CI
bug (the registry-coverage test above catches it before prod);
emitting a typed envelope with payload: null would train
consumers to tolerate broken payloads, defeating the point of
§3.4. Clean omission plus a loud log is the contract.
The envelope carries a plain type: string field; the payload
field is the discriminated oneOf union. Consumers switch on
type and narrow payload explicitly:
if (event.type === "mail.sent") {
use(event.payload as MailEventPayload);
}Envelope-level discrimination — each event-type constant pinned
as a type const in its own envelope variant, with OpenAPI 3.1
discriminators giving consumers automatic narrowing — would be
nicer. It is not the design because no current Go OpenAPI client
generator produces a workable Go type from envelope-level
oneOf:
- oapi-codegen collapses the envelope to a
json.RawMessagewrapper that loses all field access —cmd/gc/cmd_events.go's field-based construction breaks. - ogen drops
text/event-streamoperations entirely — the events streams disappear from the generated client.
The payload-field-union design is the current ceiling. Every payload variant is still fully typed on the wire; consumers narrow explicitly rather than getting automatic discriminator narrowing. See §6 for the full tooling note.
The invariants above exist so the developer's contribution to the HTTP + SSE surface is Go code only. Tooling produces everything else.
- Edit or add input/output struct types with Huma tags
(
json:"...",minLength:"1",required:"true", etc.). - Write the handler function; register via
huma.Register(or thecityGet/cityPost/cityPatch/ etc. helpers ininternal/api/city_scope.gofor per-city scoped operations). - Commit. Pre-commit regenerates
internal/api/openapi.json,docs/schema/openapi.json,internal/api/genclient/, and the TS types undercmd/gc/dashboard/web/src/generated/. Mintlify publishes the spec on the next docs build.
- Add the constant to
internal/events/events.goand append it toevents.KnownEventTypes. - Define a typed payload struct implementing
events.Payload(a trivialIsEventPayload()method), or useevents.NoPayloadfor events whose envelope fields alone capture the semantics. - Call
events.RegisterPayload(constant, sample)from aninit()in the domain package that owns the event (e.g.internal/api/event_payloads.gofor mail/bead;internal/extmsg/events.gofor extmsg). - Commit. Pre-commit regenerates the discriminated-union wire schema; generated clients gain the new typed variant automatically.
Skipping any step lands on a CI failure, not a production bug:
| Miss | Caught by |
|---|---|
| Spec not regenerated after Go-type change | TestOpenAPISpecInSync |
| Generated Go client out of sync with spec | TestGeneratedClientInSync |
| Handler response field undeclared in spec | Layer 1 response-validation tests |
| Spec/client method-shape drift | Layer 2 round-trip tests (genclient_roundtrip_test.go) |
| End-to-end binary wire regression | Layer 3 integration tests (//go:build integration) |
| New event-type constant without registered payload | TestEveryKnownEventTypeHasRegisteredPayload |
Hard-coded SPA /v0/... path outside typed client |
TypeScript build (satisfies SpecPath in api.ts) |
Principle 7's "payload-field-level discrimination rather than envelope-level" is a Go-tooling constraint, not a principled preference. The TypeScript and Go ecosystems differ on what they support; this section records what we evaluated and what we use per language.
- Huma v2 — server framework. Generates OpenAPI 3.1 from annotated Go types; we use it for every typed endpoint. Emits a 3.0 downgrade on request for consumers that still need 3.0.
- oapi-codegen — our current Go client generator. Supports
OpenAPI 3.0 (we feed it the downgrade from Huma). When given
envelope-level
oneOf, it generatesstruct { union json.RawMessage }withAsX/FromX/MergeXaccessor methods. That shape breaks field-based construction incmd/gc/cmd_events.go. It does generate typed request methods for SSE endpoints, but does not parse SSE frames — the caller handles framing. - ogen — evaluated via spike. Refuses
text/event-streamcontent type entirely; every SSE endpoint is dropped from the generated client. Withignore_not_implemented: all, ogen produces clean REST types but drops SSE operations Gas City is built on. Not viable. - openapi-generator (Java-based) — breaks the pure-Go toolchain and generates less-idiomatic Go.
- Commercial SDK generators (Speakeasy, Fern, Stainless) —
generate typed Go SSE clients including envelope-level
oneOfhandling. Not open source; paid plans start at ~$250/mo.
The payload-field-union EventPayload design (Principle 7) is the
current ceiling under open-source Go tooling. Revisit if
oapi-codegen's experimental 3.1/3.2-aware branch stabilizes or if
another open-source Go generator ships envelope-level oneOf plus
SSE that works with our shape.
openapi-fetch— typedfetchwrapper, the tool the dashboard uses for every REST call site. Typed path/body/response againstopenapi-typescript-generatedschema.d.ts. Minimal runtime, well-documented, keeps REST call-site code short. Does not handle SSE — that's what drives the dual-tool design below.@hey-api/openapi-ts— open-source generator the dashboard uses exclusively for SSE. Generates typed stream functions usingfetch()+ReadableStream(notEventSource), which means custom auth headers work, retry with exponential backoff is built in, and each stream has typed discriminated-union response types keyed by the SSEeventname.sse.tsis a thin callback bridge over the generatedstreamSupervisorEvents,streamEvents, andstreamSessionfunctions; the per-frame JSON parsing, line buffering, and retry are all framework code.openapi-typescript-codegen— unmaintained.- OpenAPI Generator (Java) — same pure-toolchain concern as Go.
The dual-tool design is pragmatic, not aspirational: each library
handles what it's good at. openapi-fetch is the minimal typed
surface for REST consumers (kept because it has zero impact on
call-site code and the ecosystem has shifted to hey-api slowly
enough that we'd gain nothing by churning every REST call today).
@hey-api/openapi-ts is the only open-source TS tool that
generates typed SSE stream clients, and it handles every aspect of
the SSE wire that used to be hand-rolled in sse.ts.
The Go-side oneOf ceiling described above does not apply to
TypeScript consumers. SSE frames come typed and discriminated
through the generated stream functions; consumers get automatic
switch (frame.event) narrowing with no hand-written parser or
type guard in the SPA.
/svc/*proxy. See §3.9.- Outbound HTTP (
internal/extmsg/http_adapter.go,internal/workspacesvc/proxy_process.go). Not typed API endpoints; we consume someone else's contract. - Storage-layer (de)serialization (SQL BLOBs, JSONL log files, external-tool auth files). Not on our wire.
- Generated Go client as a Go SDK surface. Stays in
internal/until external consumers show up. - WebSocket transport. HTTP + SSE only. OpenAPI 3.1 + Huma covers SSE end-to-end, so AsyncAPI / Modelina are not in play.
Every file-path citation in this document is load-bearing. If you
rename or remove a cited symbol (events.KnownEventTypes,
EventPayloadUnion, TestEveryKnownEventTypeHasRegisteredPayload,
cmd/gc/apiroute.go:apiClient(), addMutationCSRFParam,
registerFrameworkHeaders, sseResponseHeaders,
OptionalParam, cityinit.Service, cityinit.InitRequest,
cityinit.InitResult, cityinit.UnregisterRequest,
cityinit.UnregisterResult, cityinit.ErrNotRegistered,
TransientCityEventSource, etc.), update this document in the same
commit. Stale architecture docs are worse than no docs — they
mislead future agents about what invariants hold.
Framework-specific patterns and Huma quirks are captured in
Huma Usage Notes; update that file
in the same commit when you touch any of: OptionalParam,
addMutationCSRFParam, registerFrameworkHeaders,
sseResponseHeaders, the SSE hand-writing zone, or the
cityPost/cityRegister helper family.
Line numbers are deliberately omitted so the spec survives refactors. Package names, type names, and test names are stable anchors.