Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,32 @@ jobs:
- name: Kotlin service drift (static; jq/grep, no JVM)
run: make kt-check-drift

# Cross-SDK, and reads only committed generated files, so it needs no
# toolchain and belongs here rather than in any one SDK's job. The five
# split tables are hand-maintained copies of one mapping and nothing
# compared them TO EACH OTHER — every check-*-service-drift script
# validates one SDK against its own generator and can see no other SDK.
# doc-constants-check derives SPEC section 5's roster from the Kotlin and
# Swift accessors alone, which two omissions in the same place would leave
# agreeing.
#
# Under LC_ALL=C so CI exercises the non-UTF-8-locale path (the reads are
# pinned to UTF-8; this proves it stays that way). The accessor and Go
# client files carry non-ASCII text, so an unpinned read raises
# InvalidByteSequenceError before anything is compared.
- name: Six SDKs agree on which services exist
run: make check-service-inventory-parity
env:
LC_ALL: C

# The run above only ever exercises the PASSING case. This drives
# synthetic repository trees through the same gate, including the
# omitted-from-both-accessor-files case that is the whole reason it exists.
- name: Service inventory parity gate self-test (synthetic trees)
run: make test-check-service-inventory-parity
env:
LC_ALL: C

# The third Go surface. go-check-drift compares generated operations
# against the hand-written go/pkg/basecamp wrappers; nothing watched the
# GROUPED client until this gate, which is how ArchiveProject and
Expand Down
27 changes: 25 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1252,14 +1252,37 @@ tools:
# Spec-shape lints
#------------------------------------------------------------------------------

.PHONY: check-gradle-serialization test-check-gradle-serialization check-bucket-flat-parity validate-api-gaps check-deprecation-parity kt-check-optional-arrays-and-scalars go-check-optional-pointers test-enhance-request-reachability check-fixture-coverage check-idempotency-parity check-write-semantics-parity check-retry-metadata-parity check-runner-test-reachability check-fixture-execution check-replay-decoder-parity check-readme-env-vars test-check-readme-env-vars lint-npm-lockfile-writes test-lint-npm-lockfile-writes test-assert-sdk-built test-assert-lockfiles-unchanged check-projected-examples
.PHONY: check-gradle-serialization test-check-gradle-serialization check-bucket-flat-parity check-service-inventory-parity test-check-service-inventory-parity validate-api-gaps check-deprecation-parity kt-check-optional-arrays-and-scalars go-check-optional-pointers test-enhance-request-reachability check-fixture-coverage check-idempotency-parity check-write-semantics-parity check-retry-metadata-parity check-runner-test-reachability check-fixture-execution check-replay-decoder-parity check-readme-env-vars test-check-readme-env-vars lint-npm-lockfile-writes test-lint-npm-lockfile-writes test-assert-sdk-built test-assert-lockfiles-unchanged check-projected-examples

# Verify every bucket-scoped GET list operation has a flat-path counterpart
# (or is justified in spec/bucket-scoped-allowlist.txt). Cross-project SDK
# consumers shouldn't need to enumerate projects to reach account-wide data.
check-bucket-flat-parity:
@./scripts/check-bucket-flat-parity.sh

# Verify the six SDKs agree on WHICH services exist. The service split is one
# mapping hand-transcribed into five generator configs, and every per-SDK
# check-*-service-drift script validates ONE SDK against ITS OWN generator and
# config — the TypeScript/Ruby/Python/Swift ones by regenerate-and-diff (which
# does see service filenames), Kotlin's fast one by operationId. What none of
# them can do is look at another SDK. So a service added to three tables and
# forgotten in the other two is invisible to all of them: each SDK is faithful to
# its own table and the tables disagree. This is the cross-SDK axis, not a
# freshness check, and it does not replace them. It reads what the generators
# EMITTED rather than reimplementing the mapping, which is what keeps it from
# being a sixth copy. Committed generated files only, so no toolchain: it belongs
# in the spec-gates CI job beside kt-check-drift.
check-service-inventory-parity:
@echo "==> Checking cross-SDK service inventory parity..."
@./scripts/check-service-inventory-parity

# Drive that gate from outside with synthetic repository trees. Its live run only
# ever exercises the PASSING case, so nothing there proves it rejects anything.
# Each case builds all eight renderings in a tmpdir by inverting the gate's own
# normalization, then mutates one; the tracked tree is never written to.
test-check-service-inventory-parity:
@ruby ./scripts/test-check-service-inventory-parity.rb

# Verify @deprecated propagates to all six SDKs in the right signal class
# (compiler=Kotlin; editor=TS/Go; doc-only=Ruby/Python/Swift), that the clean
# controls stay unmarked, and that no doubled "Deprecated: Deprecated:" slips in.
Expand Down Expand Up @@ -1520,7 +1543,7 @@ check:
if [ $$rc -ne 0 ]; then exit $$rc; fi; \
echo "==> All checks passed"

check-targets: check-gradle-serialization test-check-gradle-serialization lint-actions sync-spec-version-check smithy-check smithy-mapper-test behavior-model-check provenance-check sync-api-version-check doc-constants-check url-routes-check bc3-route-parity test-bc3-route-parity go-check-drift go-check-wrapper-drift go-check-generated-drift check-grouped-client-coverage test-check-grouped-client-coverage auth-routable-check kt-check-drift swift-check-drift go-check ts-check rb-check kt-check swift-check py-check check-bucket-flat-parity validate-api-gaps check-deprecation-parity check-fixture-coverage kt-check-optional-arrays-and-scalars go-check-optional-pointers test-enhance-request-reachability check-idempotency-parity check-write-semantics-parity check-retry-metadata-parity check-runner-test-reachability conformance check-fixture-execution check-replay-decoder-parity check-readme-env-vars test-check-readme-env-vars lint-npm-lockfile-writes test-lint-npm-lockfile-writes test-assert-sdk-built test-assert-lockfiles-unchanged check-projected-examples
check-targets: check-gradle-serialization test-check-gradle-serialization lint-actions sync-spec-version-check smithy-check smithy-mapper-test behavior-model-check provenance-check sync-api-version-check doc-constants-check url-routes-check bc3-route-parity test-bc3-route-parity go-check-drift go-check-wrapper-drift go-check-generated-drift check-grouped-client-coverage test-check-grouped-client-coverage auth-routable-check check-service-inventory-parity test-check-service-inventory-parity kt-check-drift swift-check-drift go-check ts-check rb-check kt-check swift-check py-check check-bucket-flat-parity validate-api-gaps check-deprecation-parity check-fixture-coverage kt-check-optional-arrays-and-scalars go-check-optional-pointers test-enhance-request-reachability check-idempotency-parity check-write-semantics-parity check-retry-metadata-parity check-runner-test-reachability conformance check-fixture-execution check-replay-decoder-parity check-readme-env-vars test-check-readme-env-vars lint-npm-lockfile-writes test-lint-npm-lockfile-writes test-assert-sdk-built test-assert-lockfiles-unchanged check-projected-examples
@:

# Clean all build artifacts
Expand Down
10 changes: 6 additions & 4 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ That roster is the canonical surface, not a per-SDK inventory. Accessor counts v

### Derivation Rule `[static]`

The OpenAPI spec groups operations under coarse tags (e.g., `Automation`, `Todos`, `Files`). The service generators split those tags into the `53` fine-grained services above <!-- @service-count --> using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`, `HillCharts`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`, `CloudFiles`, `GoogleDocuments`. Both examples are exhaustive on purpose: an abridged one is how `cloudFiles` and `googleDocuments` stayed invisible to this section for so long — a service that arrives through a split rather than a tag of its own is named nowhere a reader would look. These mappings are defined in each language's generator script. They are five hand-maintained copies of one table with no gate comparing them, so they are expected to produce identical service sets and are not guaranteed to: Appendix F records where they currently do not.
The OpenAPI spec groups operations under coarse tags (e.g., `Automation`, `Todos`, `Files`). The service generators split those tags into the `53` fine-grained services above <!-- @service-count --> using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`, `HillCharts`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`, `CloudFiles`, `GoogleDocuments`. Both examples are exhaustive on purpose: an abridged one is how `cloudFiles` and `googleDocuments` stayed invisible to this section for so long — a service that arrives through a split rather than a tag of its own is named nowhere a reader would look. These mappings are defined in each language's generator script. They are five hand-maintained copies of one table, and `make check-service-inventory-parity` compares what those copies **emitted** — the TypeScript, Ruby, Kotlin and Swift generated service directories, Python's generated `__init__.py` barrel, the two generated accessor files this section's roster is derived from, and Go's hand-written accessors — so identical service sets are enforced rather than merely expected. It reads what each generator already emitted rather than reimplementing the mappings, which is what keeps it from being a sixth copy — with Go the one exception, having no generated per-service files, so its hand-written accessors are compared against the others' generated output and carry the carve-outs noted below. (Python is read from its barrel because its generator, alone among the five, does not delete outputs a mapping stopped producing; the barrel is rewritten whole every run and so cannot name a service that is no longer emitted.) Each per-SDK `check-*-service-drift` script remains the freshness gate for its own SDK; none of them can see another SDK, which is the axis this one adds. Go's three divergences (it folds `automation` and `clientVisibility` into other services and spells `timesheets` singular) are stated as data in that gate and fail it if they ever stop applying; Appendix F records them.

### Merge-Safe Write Surface (Cards)

Expand Down Expand Up @@ -3649,9 +3649,11 @@ roster. The Kotlin and Swift rows are marked, because §5's roster is derived fr
exactly those two files and a restatement of a gated value has to be gated too.
Of the other four, Python's is held by its own accessor-inventory test, which
derives the roster from `python/src/basecamp/generated/services/` and fails when
an accessor is missing. The remaining three are hand-verified and dated below —
no gate derives them, and building one means a sixth hand-copy of each
generator's split tables.
an accessor is missing, and Go's is read by `make check-service-inventory-parity`
— including the three carve-outs its row states, which that gate fails if they
stop applying. Ruby's and TypeScript's client wiring is hand-verified and dated
below; the parity gate reads their generated service directories, which is what
each generator emitted rather than what the client exposes.

| SDK | Account-scoped services |
|-----|------------------------|
Expand Down
Loading
Loading