Skip to content

Uploads: replace a file in place, and say which file each version is - #683

Merged
jeremy merged 17 commits into
mainfrom
upload-versions-api
Aug 7, 2026
Merged

Uploads: replace a file in place, and say which file each version is#683
jeremy merged 17 commits into
mainfrom
upload-versions-api

Conversation

@jeremy

@jeremy jeremy commented Aug 6, 2026

Copy link
Copy Markdown
Member

Absorbs basecamp/bc3#12555 and #12565. Closes #649. Closes the write side of spec/api-gaps/upload-new-version.md, open since 2026-07-22, and unblocks basecamp-cli#404.

The write

POST /uploads/{id}/versions.json replaces an upload's file in place. The recording keeps its id, its URL and its comments; the previous file becomes a past version. CreateUpload cannot do this — it mints a new recording with a new id and a new URL, so every published link rots on each release.

CreateUploadVersion models attachable_sgid, base_name, description, notify and subscriptions.

notify and subscriptions are documented contracts as of #12565, not guesses. Subscribers#notify_param defaults to "custom", so an audience arrives through two doors: notify naming a mode, or a bare subscriptions array taken as custom. The plan for this work assumed neither was caller-selectable; reading the concern showed the second door is live and that bc3's own web tests already rely on it. visible_to_clients is deliberately absent — #12565 removed it from the endpoint's reachable surface, because it never set the recording's visibility, only widened the notification audience, and could announce a client-invisible file to a project's clients.

The read — this is the half that fixes #649

ListUploadVersionsOutput declared uploads: UploadList. The endpoint returns events: 11 of Upload's 14 @required members are absent from every response. That typed lie is why the CLI's versions command and the MCP server's list_upload_versions render blank fields — the SDK was decoding an event payload into a struct whose required fields it does not carry.

It now returns UploadVersion / UploadVersionFile, built from _version.json.jbuilder over recordings/events/_event.json.jbuilder rather than from the Upload shape it was pretending to be. New shapes rather than EventList plus a member, for the reason bc3's own commit gives for using a purpose-built partial: widening the shared one "would leak upload fields onto todo, message, and card events". Recorded in SPEC §10 as the second worked example of One Renderer, One Schema.

The 507, which turned out to be four operations wide and then six

StorageLimitError declares the 507 ensure_account_can_upload_files has always been able to raise. That guard fronts four modelled operations, so all four take it — absorbing the contract for the new one alone would leave three declaring a status they can return and don't model.

The larger find is the classification. SPEC §6 had no 507 step, so a 507 fell through status >= 500 and surfaced as api_error with retryable: true — a plan limit no retry can satisfy, reported as a transient server error. §6 now maps 507 to limit_exceeded, non-retryable, ordered ahead of the 5xx catch-all, with exit code 10 in all six SDKs.

That also fixes ProjectLimitError, which #679 added days ago. Five SDKs carried tests pinning the old behaviour, each commented "No SDK gives 507 a named class". Python's went further and recorded a genuine cross-SDK divergence — its fallback arm produced retryable=False while the other five marked every unclassified 5xx retryable, "asserted here as-is rather than fixed in passing". All six now agree on False, and False is what the spec says rather than an accident of which arm caught the status.

Nothing retried a 507 in practice — no retryOn list names it — so this changes what the caller is told, not what the client does.

Breaking

Two entries in MIGRATING.md. The ListUploadVersions retype is compiler-silent in Ruby, Python and Kotlin: the type does not change, only which keys are actually present. Go's UpdateUploadRequest.Description*string the compiler does catch.

BaseName stays a plain string on both request types, and says why in its doc comment: Upload#base_name= guards on new_base_name.present?, so "" and absent are the same server write and there is no third state a pointer could express.

Verification

make passes clean. 250 operations.

Every new assertion was run against un-fixed code first:

  • TestUpdateUploadRequest_DescriptionIsTriStateDescription: the clear spelling must reach the wire
  • TestUploadVersionFromGenerated_DetailsSurvivesa present but empty details object must survive as non-nil
  • the conformance 507 case → TypeScript reported expected 'api_error' to be 'limit_exceeded'

conformance/tests/uploads_write.json — 7 cases, all six runners green. Named for the surface, not the operation, because it has to cover both writes: UpdateUpload lands on the same serialized ActionText attribute and had its presence semantics pinned nowhere. The uploads surface had zero requestBodyAbsent assertions before this; each rides with a requestBody presence assertion in the same case, since a requestBodyAbsent alone is satisfied by an empty body.

The shared fixture is built from the renderer, and its mock bodies come from spec/fixtures/uploads/get.json — Swift's strict decoder rejected hand-rolled ones for missing bucket, which is a better outcome than passing.

Notes for review

Still open


Summary by cubic

Adds CreateUploadVersion to replace an upload’s file in place and retypes versions reads to return UploadVersion with the file each event recorded. Also reclassifies HTTP 507 to non‑retryable limit_exceeded across SDKs and sweeps docs to the correct 250‑operation count.

  • New Features

    • POST /uploads/{id}/versions.json via CreateUploadVersion; models attachable_sgid, base_name, description, notify, subscriptions.
    • Description is presence‑aware: omit carries forward, "" clears, value sets.
    • Versions list returns UploadVersion with upload details and current. current is positional and exactly one per non‑empty response; it does not mean “the file the upload’s download_url serves” after a metadata‑only update.
  • Migration

    • Type change: listVersions now returns UploadVersion (not Upload) in TypeScript, Swift, and Kotlin; update callers. Ruby/Python decode unchanged at runtime.
    • New error code: 507 is limit_exceeded (exit code 10), non‑retryable. Update exhaustive handling in TypeScript, Swift, Kotlin, and Python; applies to eight ops: CreateUpload, CreateUploadVersion, CreateDocument, CreateCloudFile, CreateProject, UnarchiveProject, CreateWebhook, UpdateWebhook.
    • TypeScript: UploadVersion and CreateVersionUploadRequest are exported from index.ts; update imports if used.
    • Go: UpdateUploadRequest.Description is now *string; 507 maps to limit_exceeded on generated and raw HTTP paths.
    • Selecting past versions: filter for upload.current === false; do not filter on action === "blob_changed".
    • Docs/counts sweep: repo updated to 250 operations (83 idempotent, 167 non‑idempotent); 42 POSTs are attempted exactly once.

Written for commit 1797145. Summary will update on new commits.

Review in cubic

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cff43584a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread typescript/src/generated/services/uploads.ts Outdated
Comment thread kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/uploads.kt Outdated
Comment thread MIGRATING.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings August 7, 2026 10:01
@github-actions github-actions Bot added typescript Pull requests that update TypeScript code ruby Pull requests that update the Ruby SDK go kotlin swift spec Changes to the Smithy spec or OpenAPI conformance Conformance test suite python Pull requests that update the Python SDK labels Aug 7, 2026
@jeremy

jeremy commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

All three Codex findings addressed in 6be4404, and two of them were catches I would not have made.

The generator fix (threads 1 and 2). UploadVersion is now registered in the TypeScript and Kotlin TYPE_ALIASES, so listVersions returns ListResult<UploadVersion> in both — fixed in the generators, not the generated files.

Checked against origin/main, both were regressions I introduced:

before after my change now
TypeScript ListResult<Upload> raw response-schema type ListResult<UploadVersion>
Kotlin ListResult<Upload> ListResult<JsonElement> ListResult<UploadVersion>

The TypeScript one was silent: requestPaginated still returned a ListResult at runtime, so .meta.totalCount kept working while no longer type-checking, and my tests missed it because they only index the array.

The Kotlin one also falsified something I had written down. MIGRATING said that row was "unchanged — bare-array responses were already untyped here"; I had grepped the file after the change and read the result as the prior state. Corrected, and Kotlin now emits the models, so the conformance summarizer reads decoded values — which makes that case a real decode test there, as it already was in Swift.

The source break (thread 3). MIGRATING now carries a per-SDK table for the new error code: TypeScript's ErrorCode union, Swift's enum case, Kotlin's sealed subtype. This repo's own Kotlin ErrorTest failed to compile on exactly that during development, so it is demonstrated rather than asserted. It also now notes the reclassification reaches CreateProject/UnarchiveProject, which shipped in v0.13.0 as retryable api_error — the mapping is by status, not by operation.

make passes clean; all 7 uploads_write.json cases still green across all six runners.

Copilot errored rather than reviewing and has been re-requested.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6be4404164

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread typescript/src/generated/services/uploads.ts
Comment thread MIGRATING.md Outdated
Copilot AI review requested due to automatic review settings August 7, 2026 10:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86cecb9784

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/helpers.go
Copilot AI review requested due to automatic review settings August 7, 2026 10:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@jeremy

jeremy commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy

jeremy commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

CI note: the npm Audit (TypeScript SDK) failure is not from this PR.

GHSA-5p4m-2wfm-xmqj (high) landed on js-yaml 4.0.0–4.3.0. It reaches us transitively through a dev tool:

openapi-typescript → @redocly/openapi-core@1.34.6 → js-yaml@4.3.0

git diff --stat origin/main..HEAD -- typescript/package.json typescript/package-lock.json is empty — this branch does not touch TypeScript dependencies at all, and the advisory postdates main's last green Security run (887adbc03a, 2026-08-06). It would fail on main today.

#685 already owns the fix ("Bump the js-yaml override past the !!omap advisory"), following the same override pattern as #624 for fast-uri. Deliberately not fixing it here — a dependency bump in a feature PR is lockfile churn in the wrong place, and this repo has consistently kept them separate.

Everything else on 60eedb81 is green; Analyze (swift) was still pending at the time of writing.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60eedb819a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread spec/basecamp.smithy Outdated
Copilot AI review requested due to automatic review settings August 7, 2026 11:02
@jeremy

jeremy commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI review requested due to automatic review settings August 7, 2026 13:22
@jeremy

jeremy commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a72e79e558

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread MIGRATING.md Outdated
The reclassification is keyed on status, so it reaches every operation the
spec gives a 507 — including CreateWebhook and UpdateWebhook, which have
declared WebhookLimitError since long before this branch and were reporting
it as a retryable api_error the whole time.

Webhook and project callers therefore need the same new branch even though
nothing about those endpoints changed, which is exactly the kind of thing a
migration guide exists to say and mine did not.

The list is now a table of all eight across the three limits, derived from
openapi.json rather than recalled, with the query to re-derive it. That is
also what makes the omission embarrassing rather than subtle: the six
README tables this PR already updated say "file storage, projects,
webhooks" — I knew the set and then wrote a shorter one here.
Copilot AI review requested due to automatic review settings August 7, 2026 13:39
@jeremy

jeremy commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6dc3788312

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread behavior-model.json
Third round on this class, so this time the sweep is the fix rather than
the two references Codex pointed at.

SECURITY.md said 249 operations and 41 non-idempotent POSTs; both move to
250 and 42. The rest of that paragraph checks out and is untouched — 125
GETs, 83 idempotent mutations, 52 PUTs, 24 DELETEs, 7 flagged POSTs, and
the seven named are exactly the seven behavior-model flags. Verified rather
than assumed, since the whole point of the last two rounds is that I
patched what I was shown and not what was true.

scripts/check-grouped-client-coverage's floor comment cited 249 as the real
value. Not load-bearing — MIN_OPERATIONS is 200 and catches extraction
collapse, not drift — but it is a current-value claim in prose and it was
wrong.

The sweep: every tracked file except MIGRATING (as-of history), generated
output and lockfiles, scanned for 247/248/249 within ninety characters of
"operation", "POST", "route" or "surface". Zero remaining.
Copilot AI review requested due to automatic review settings August 7, 2026 13:57
@jeremy

jeremy commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 179714564a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jeremy
jeremy merged commit b238e5e into main Aug 7, 2026
49 of 50 checks passed
@jeremy
jeremy deleted the upload-versions-api branch August 7, 2026 19:18
jeremy added a commit that referenced this pull request Aug 7, 2026
#686)

* Mark the operation count, so it stops being six hand-maintained copies

250 is one jq away from openapi.json and was restated in prose six times
across four files. Adding one operation left five of them stale, and it
took three rounds of review on #683 to reconcile — each round finding the
copies the previous round had not been shown.

@operation-count joins @api-version, @bc3-pin and @assertion-types in
spec/doc-constants.json's existing machinery: `make sync-api-version`
rewrites every marked span from the source, and `make doc-constants-check`
fails on any that drifted. markerCounts pins six, so deleting one fails
the gate rather than silencing it.

The span must state the count as a BACKTICKED integer, and exactly one.
Those sentences are full of other numbers — SECURITY.md names 125 GETs and
83 mutations beside the total — and a checker that read bare prose integers
as the claim would fail on numbers it has no source for. Backticks are how
the prose says "this is the derived constant", which is the device @bc3-pin
already uses for the SHA. A line needing a second backticked integer cannot
carry the marker, and the error says so.

Counting is (path, HTTP method) pairs. Path items also carry parameters,
summary and servers, so the verb list is explicit rather than "every key".

Derived lazily. The gate's own fixtures are minimal OpenAPI documents with
no .paths, and computing eagerly turned each into an error about a constant
it never mentions.

Six self-test cases, each shown to fail against the un-built feature:
neutering the checker fails the three negative cases, neutering the
rewriter fails the writer case, and counting every path-item key fails the
positive control. The fixture's real operations live in the --openapi
source with a five-operation decoy in the checkout, so a gate reading the
wrong file reports 5 against a span that says 3.

End-to-end: drifting all six spans to 999 flags six errors, and the writer
restores all three files byte-identical.

* The writer must refuse an ambiguous span, not rewrite every integer on it

Codex found a real hole, and it was the dangerous kind: the writer
corrupted data and the checker then certified the corruption.

--write returns before the per-kind checkers run. So a blanket gsub over
every backticked integer on the marked line ran FIRST, and the later check
— comparing values that were by then all identical, because it deduplicated
— went green over the damage.

Reproduced on the real sentence before fixing. Backticking SECURITY.md's
125 GETs and 83 mutations, then `--write`:

  before: all `250` operations: the `125` GETs ... and `83` mutations
  after:  all `250` operations: the `250` GETs ... and `250` mutations
  check:  passes

Two changes. The count is now found by one helper both the checker and the
writer call, so they cannot disagree about which integer is the claim; and
that helper counts OCCURRENCES rather than distinct values, because two
spans both reading `250` today would both be rewritten the day the count
moves and only one of them is the count.

An ambiguous span is now left exactly as written. That is deliberate rather
than a silent skip: untouched, it fails the next --check with a message
naming the integers it found and what to do about them.

The self-test drives --write over an ambiguous span, asserts the other
integer survived, and then runs --check in the same directory to assert the
span is still rejected. Restoring the blanket gsub fails it.
@jeremy jeremy added the breaking Breaking change to public API label Aug 11, 2026
jeremy added a commit that referenced this pull request Aug 11, 2026
…s canary (#698)

* Sync all six SDK-version lockfiles on bump, and make release's preflight agree

bump-version.sh synced four of the six lockfiles that record the SDK's own
version through a path dependency. The conformance Ruby and Python runner
lockfiles were missed, and both are gitignored — so on any machine that had
run conformance at the old version, the first post-bump make check re-resolved
them mid-check and assert-lockfiles-unchanged correctly failed (#671).

Sync both in the bump, mirroring the conformance TypeScript step, and extend
make release's preflight to assert them too — guarded on existence, since a
fresh clone legitimately lacks them. The preflight and make check now agree on
what "in sync" means.

Closes #671

* Record the upload-versions live canary as done

Ran 2026-08-11 against production (account 2914079, Coworker QA Sandbox vault)
via a throwaway Go program built on this repo's go/ module. Every deferred
assertion from the #683 plan passed: stable id/URL across CreateVersion, typed
ListVersions decode with exactly one Current entry, byte-compared downloads of
both the original and the replacement, and description carry-forward.
jeremy added a commit that referenced this pull request Aug 13, 2026
SPEC §19's Test Categories table and Appendix D each claim to account for
every fixture under conformance/tests/, and both had drifted. The last
three fixture-adding commits missed the convention in three different
ways: dee221c (#601) added documents_write.json and updated neither
table; b238e5e (#683) added uploads_write.json with four Appendix D
rows and no §19 row; #726 added search.json's §19 row and missed
Appendix D. Two half-applications in opposite directions is not
carelessness — CONTRIBUTING.md tells contributors to add conformance
tests and mentions neither table.

Each cell is derived from the fixture's own description citations, which
is the convention the existing rows follow. documents_write.json cites
"SPEC 18 body compaction" and "SPEC 18 rule 6", and §5's Documents
subsection already back-references it; uploads_write.json cites "SPEC
§18", "SPEC.md §5" and "SPEC §6 step 11", the same four attributions its
Appendix D rows already spell out.

Also moves the search row into its sorted position, where #726 misfiled
it between retry and schedule-entries-write.

Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn
jeremy added a commit that referenced this pull request Aug 13, 2026
…them (#740)

* Account for every conformance fixture in both SPEC rosters

SPEC §19's Test Categories table and Appendix D each claim to account for
every fixture under conformance/tests/, and both had drifted. The last
three fixture-adding commits missed the convention in three different
ways: dee221c (#601) added documents_write.json and updated neither
table; b238e5e (#683) added uploads_write.json with four Appendix D
rows and no §19 row; #726 added search.json's §19 row and missed
Appendix D. Two half-applications in opposite directions is not
carelessness — CONTRIBUTING.md tells contributors to add conformance
tests and mentions neither table.

Each cell is derived from the fixture's own description citations, which
is the convention the existing rows follow. documents_write.json cites
"SPEC 18 body compaction" and "SPEC 18 rule 6", and §5's Documents
subsection already back-references it; uploads_write.json cites "SPEC
§18", "SPEC.md §5" and "SPEC §6 step 11", the same four attributions its
Appendix D rows already spell out.

Also moves the search row into its sorted position, where #726 misfiled
it between retry and schedule-entries-write.

Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn

* Gate both conformance-fixture rosters against git ls-files

`sync-doc-constants.rb` already does table-completeness checking:
@assertion-types wraps SPEC §19's assertion-type table in a block marker
and set-compares it against conformance/schema.json. The two fixture
rosters are the same shape one level out — a table that claims to
account for every fixture under conformance/tests/, with nothing
checking it — so they become two more block kinds rather than a new
script and a new CI step. `make doc-constants-check` already runs the
gate and its self-test, and is already in check-targets and spec-gates.

The source is `git ls-files conformance/tests/*.json`, not Dir.glob, for
tracked_markdown's reason: an untracked scratch fixture must not fail a
developer's build. Direct children only — git's pathspec `*` matches
across `/`, and a nested fixture is discovered by no runner, so
demanding a roster row for it would be documenting a claim that is not
true. That scope is also how SPEC §23's carve-out is honored:
conformance/oauth/, oauth-token/ and event-feed*/ are documented at
their own section and directory.

The two invariants differ because the artifacts differ. §19's table is a
bijection, so all of it is asserted: one row per fixture, both
directions, and category slug == basename with `_` as `-` (verified
across all 22 rows). Appendix D's rows are curated summaries that
deliberately bundle several cases — uploads_write.json legitimately has
four — so it gets coverage only, and a self-test case pins that
difference by asserting several rows for one fixture still passes.

Both tables also reject a row whose attribution cell is blank, and a
`§N` reference that resolves to no `## §N.` heading — the latter catching
a reference that resolved when written and stopped resolving when a
section was renumbered, which a reviewer of the same PR cannot see. A row
with no section reference at all is still accepted, because rejecting it
needs a carve-out for live-my-surface.json's external-governance
attribution and the carve-out list is the part that grows.

Neither table is writable: --write only ever touched line spans, and a
row here carries an owning-section attribution or a case summary only
the fixture's author can make.

Both checks reject SPEC.md as it stood before the preceding commit:

  SPEC.md:2110-2131: conformance/tests holds 22 tracked fixture(s), the
    table categorises 20; missing: `documents_write.json`,
    `uploads_write.json`.
  SPEC.md:3379-3458: no row maps these tracked fixtures to a primary
    section: `documents_write.json`, `search.json`.

Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn

* Keep block bodies under the pin scan, and reject colliding category slugs

Two review findings on the roster gate, both real interactions rather than
Markdown-spelling edge cases.

Block span bodies were dropped from the prose pool along with line spans, but
the two are not alike. The writer rewrites line spans only, and the block
checkers read nothing but the `|` rows, so an ordinary sentence parked inside
a roster or assertion-types block survives both untouched. Excluding the whole
body let "verified against <current pin>" sit there with no marker and no
grant — invisible to check_unmarked_pin and silently stale at the next repin,
which is the exact claim class this gate exists to catch, hidden by the gate's
own span bookkeeping. Only line spans leave the pool now.

The §19 categories table tallied FILES, which catches one fixture on two rows
but not two fixtures deriving one category. `_` and `-` collapse to the same
slug, so `foo_bar.json` and `foo-bar.json` each satisfy the per-row slug rule
while the table stops being the bijection its heading asserts. Now tallied on
the DERIVED slug — a row whose category cell is simply wrong is already
reported and still reaches that tally, so grouping by the declared cell would
both miss real collisions and invent false ones. Appendix D is unaffected: it
has no category column and deliberately allows many rows per fixture.

Both self-test cases were shown to fail against the un-fixed gate first —
reverting each fix in turn leaves exactly its own case reporting "expected
FAILURE, gate exited 0".

* Stop SPEC promising a gate this PR removed, and correct a comment inviting deletion

Codex raised the first as a P1 and Copilot as a suppressed comment; both were
right, and it is the defect class this PR family exists to prevent — prose
claiming coverage CI cannot provide.

SPEC §19 said `make check-fixture-execution` (#602) "is what detects it now".
That gate was the source-text parser split out of this PR to
conformance-skips-parser-archive; the prose describing it stayed behind. There
is no such script and no such target — `make -n check-fixture-execution` exits
"No rule to make target" — so the paragraph promised all-six detection that
does not exist, while #602 is still open.

Replaced with what is actually true: each runner's case census (#742) catches a
case executed by no runner for a MECHANICAL reason, and explicitly does not
catch the deliberate all-six exclusion this section describes, because each
census counts its own skip and stays green. The roster below it is restated
rather than derived, and nothing checks it (#736) — which is why #736 waits for
#602's cross-runner manifest instead of being fixed on its own.

Separately, roster_vacuity's comment claimed the guard "buys no coverage". That
is true only when ONE side is empty. When BOTH are, `missing` and `extra` are
both empty, the comparison is trivially satisfied, and this guard is the only
thing refusing the vacuous pass — a committed self-test case covers exactly
that. The comment as written invited deleting a live guard on the strength of
reasoning that applies to a different case.

* Require exactly three cells per roster row, and correct a self-falsifying claim

Copilot raised the cell count three times across two rounds; taking it, because
it asks for something different from the Markdown-spelling findings declined
alongside it.

Those asked the splitter to UNDERSTAND more Markdown — separator widths,
backslash parity. This asks it to REFUSE what it does not understand, which is
the direction this file already argues for: "a row the parser cannot see is a
row it silently vouches for." And it closes the pipe class as a class rather
than one spelling at a time — however a stray pipe was written, the cell count
is wrong and the row fails loudly instead of being mis-parsed quietly.

The consequence was real, not cosmetic. A raw pipe in an attribution shifts the
real section into a fourth cell and leaves the fragment before it in cells[2],
where non-`§` attributions are legitimately allowed — so the gate validated the
wrong cell and a `§99` in the actual section position was never checked, on a
gate whose whole claim is that it validates every section reference. Both
tables had it; Appendix D's free-form summaries are the likeliest place for
someone to write `supports A | B`.

Self-test cases added for both tables and shown to fail against `< 3` first.

CONTRIBUTING.md separately claimed the checklist item "is the only place this
convention was written down" — false the moment this PR also stated it in SPEC
§19 and the gate. Rephrased as the historical absence it describes.
jeremy added a commit to basecamp/basecamp-cli that referenced this pull request Aug 31, 2026
…st honesty

Four small fixes ahead of v0.10.0:

- .github/release.yml excluded the label `github-actions`, which does not
  exist in this repo; the real label is `github_actions` (28 merged PRs
  carry it, none carry the hyphen form). Latent today because the author
  exclusion catches dependabot, but a human-authored CI PR would have
  leaked into "Other Changes".

- installer-smoke.yml gains a `recover` job: on a fully green run it
  closes any open "Installer canary failure" issue, using the same
  exact-title lookup as the notify job. Without it a single blip leaves a
  permanently open issue — #644 sat open ten days across ten green runs.

- API-COVERAGE.md's header had drifted three SDK bumps behind the pin:
  it claimed v0.12.0 (actual: v0.15.0), still carried the
  uploads-versions gap that closed with v0.14.0's typed ListVersions
  (basecamp/basecamp-sdk#683 — the `uploads` row already documented the
  shipped command), and said the field-keyed 422 fix was "past this pin"
  when #541 landed inside it at v0.13.0. Summary is now 184/184 with the
  Blocked status retained at zero. The endpoint-count reconciliation the
  file defers stays deferred.

- check-cli-surface-diff.sh's header said to clear .surface-breaking
  after each release; five months and several releases in, nobody ever
  has, RELEASING.md never mentions it, and nothing depends on it. The
  comment now describes the allowlist as what it is: cumulative.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change to public API conformance Conformance test suite go kotlin python Pull requests that update the Python SDK ruby Pull requests that update the Ruby SDK spec Changes to the Smithy spec or OpenAPI swift typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ListUploadVersions declares UploadList but BC3 returns recording events — 11 of 14 required members absent

2 participants