Tier-1 spec debt: §18 carve-out names inexpressibility, Kotlin attempt floor, counted-claims rule

Merged
JE
2mo ago
+1066-306

Three tier-1 modelling/spec-debt items, one commit per issue (plus review follow-ups). No regeneration — no spec or generated files touched.

Closes #653. Closes #661. Closes #666.

§18 rule 1 carve-out: sanctioned by inexpressibility, and the class closed (#653)

The carve-out's old wording keyed on "always-send-empty semantics" via "the composite's private transport" — both wrong against the code (most census sites are public methods, and pointer members can express empties). SPEC.md §18 rule 1 and AGENTS.md invariant 4 now key on wire contract inexpressible through the generated request type, keeping every load-bearing constraint (*WithBody routing, keys match the generated schema, express-it-if-you-can) — and the repository now actually satisfies the stated rule.

Final census: 9 sites converted to generated request types, 2 surviving with the mechanism proven. Every conversion is pinned byte-for-byte with captured-body tests whose expectations were recorded against the old map implementations (25 shapes across every builder branch, including ""-clears, explicit-[] lists, and the merge-safe composites' overlay semantics — Go conformance 192/192) and bite-proved:

  • ProjectsService.Update, CheckinsService.CreateQuestion/UpdateQuestion (review finding: QuestionSchedule is all-pointer omitempty and the BC3 contract never needs explicit JSON null; questionScheduleToMap deleted). Out-of-range schedule ints are rejected with a usage error instead of silently wrapping (gosec G115, also flagged by both reviewers).
  • Round 3 (review finding): replaceTodo, replaceDocument, replaceScheduleEntry, replaceTodolistOrGroup, UpdateMyProfile, UpdateAnswer — all expressible via pointer members.

The two survivors — CardsService.UpdateVerbatim and CardStepsService.Update — are provably stuck, not asserted: their explicit due-date clear is spelled "due_on": "" (the one encoding all six SDKs share; null would violate §18 body compaction), and TestDatePointerCannotSpellEmptyDueOnClear exhausts a *types.Date member's three spellings (absent / null / real date) to show none produces it. The carve-out prose names exactly that instance, plus the counter-rule: an empty value behind a pointer member survives omitempty and does not qualify. A future Date | "" union in the generator would dissolve even these two — spec/generator enhancement, not debt in this class.

go test ./..., make go-check-drift (250/250), gosec (CI's exact invocation, 0 issues), make conformance-go (192/192), make doc-constants-check green.

Kotlin attempt floor independent of loop shape (#661)

Kotlin's budget expression was min(max(1, cap), op_max ?: cap) — for a hand-written caller with no operationName at maxRetries = 0, the budget computed 0 and only the loop's post-check shape saved it (SPEC.md carried a divergence paragraph documenting exactly this). The fallback is now ?: Int.MAX_VALUE, making the budget min(max(1, cap), op_max) with an absent ceiling meaning no ceiling — the floor is a property of the expression, not the loop. SPEC.md's divergence sentence is deleted and Kotlin joins the floored group.

Honesty note on the red proof: the latent bug is not behaviorally observable today (the post-check loop makes budgets 0 and 1 identical), so the expression was extracted to an assertable computeMaxAttempts and the red is value-level — computeMaxAttempts(0, null) expected 1, was 0 under the old semantics — stated as such in the test comment, plus a MockEngine test pinning one request for the ungoverned zero-cap path. Full :basecamp-sdk:jvmTest green.

Deferred (will note on the issue): a six-runner max_retries: 0 conformance fixture.

MIGRATING.md counted claims need a derivation rule (#666)

The per-SDK "breaking changes" count column existed in exactly one release section (v0.13.0) with no stated derivation — unlike columns A/B, whose rules make them checkable. The column is dropped (v0.14.0 already shipped without one), the one hand-incremented reference to its values is rewritten, and the "How these are counted" block now states the contract: counts appear in this document only with a stated derivation rule.

Full make check green on this branch.