Point your Bitbucket client (e.g. the 'bitbucket' npm package) baseUrl at https://bb.sandboxapis.dev. Paths mirror api.bitbucket.org/2.0.
Coverage badge
Deep114/161 · 71%
The share of read rows with a final answer — served and verified, a deviation, retired upstream, or a reviewed empty/refusal. Computed in the coverage manifest, copied here.
bb.sandboxapis.devServed & verified
106 / 66%
Answering with real universe data, each response checked against Bitbucket's published spec by the conformance suite on this build. 3 of them are marked a deviation — served faithfully, but failing the vendored spec.
The full read API
161
Every read surface Bitbucket publishes, deferred long tail included, minus the rows excluded by policy (18). Writes are out of scope: this universe is read-only.
Not served yet
55
Each returns an explicit, provider-shaped coverage error naming the gap — never invented data.
01 / The swap
No SDK of ours, no shim, no recorded fixtures. The same client library you already use, one environment variable different.
curl https://bb.sandboxapis.dev/2.0/repositories/olympus-labs/parthenonVerified drop-in clients
No client library is version-pinned against Bitbucket yet, so this page claims none. What is asserted is the wire: every response above is validated against Bitbucket's own published spec by the conformance suite. Client pins live in coverage/client-pins.yaml and are added as each SDK joins the suite.
02 / Coverage by family
All 179 rows the coverage manifest carries for Bitbucket, deferred long tail included and nothing capped. Open a family, or filter by path to find the exact endpoint your client calls.
Status — what a conformance test found
Mode — what kind of answer a row gets
A row with no mode shown has not been judged yet. Modes are the manifest's own words, from coverage/MODES.yaml; every empty and refuse carries a written reason and a review date before it counts as final.
Badge — where Deep starts
REST + GraphQL + git rows that are not `excluded`. Write operations are NOT rows (DECISIONS 2026-09-01 decision 9): they are counted in meta.write_operations and never enter this ratio.
179 read surfaces in 22 families
SERVED 2026-09-12 (hello-19 wave W-a): `ci_binding` - the same canon GitHub serves as Actions secrets and variables, in Bitbucket's dialect. `secured` IS canon's `masked`, so the two hosts cannot disagree about which of this organization's settings may be published, and `value` is `""` on a secured row because the vendored property description says so in as many words ("If the variable is secured, this will be empty") - canon holds NO value for a masked binding, so the empty string is composed at the boundary from the absence. `deployment_variable` at a deployment ENVIRONMENT, resolved through the same `envUuid` `/environments` publishes - so a uuid read off that list is one this row answers for
SERVED 2026-09-09: reader.listWorkflowSchedules(repo) (packages/artifact/src/reader.ts:2297), rendered as `pipeline_schedule` and validated against the vendored `paginated_pipeline_schedules`. `cron_pattern` is canon's (per_hour, hours_of_day, days_of_week) triple re-encoded as the seven Quartz fields the document asks for — lossless, and the conformance entry re-derives it; `target.ref_name` is the branch canon points at, and the test fetches it back through /refs/branches to prove it resolves (invariant #5). `enabled` is true because every schedule here started real runs. Answers the hello-15 generation gap on an older artifact rather than an empty envelope
SERVED 2026-09-09: reader.workflowSchedule(id) (packages/artifact/src/reader.ts:2302), byte-identical to the row the list serves — asserted, so the two addressings cannot drift. A uuid this repository does not hold is Bitbucket's own 404
SERVED 2026-09-09: reader.runsForSchedule(id) (packages/artifact/src/reader.ts:2839), reading workflow_run.schedule directly, as `pipeline_schedule_execution_executed` — the variant the vendored spec declares, whose only members are the discriminator and `pipeline`. Each pipeline is BYTE-IDENTICAL to what GET /pipelines/{uuid} serves for the same run (the conformance entry fetches it back and compares), and every run it returns carries event `schedule`. The errored variant is never emitted: canon records no schedule that failed to start a pipeline
SERVED 2026-09-12 (hello-19 wave W-a): `ci_binding` - the same canon GitHub serves as Actions secrets and variables, in Bitbucket's dialect. `secured` IS canon's `masked`, so the two hosts cannot disagree about which of this organization's settings may be published, and `value` is `""` on a secured row because the vendored property description says so in as many words ("If the variable is secured, this will be empty") - canon holds NO value for a masked binding, so the empty string is composed at the boundary from the absence. The repository's own pipeline variables, four of them: two credential-backed secrets (the registry pull token, and the integration-suite token whose two dates are a rotation canon records) and two published settings read from the rows they name
SERVED 2026-09-12 (hello-19 wave W-a): `ci_binding` - the same canon GitHub serves as Actions secrets and variables, in Bitbucket's dialect. `secured` IS canon's `masked`, so the two hosts cannot disagree about which of this organization's settings may be published, and `value` is `""` on a secured row because the vendored property description says so in as many words ("If the variable is secured, this will be empty") - canon holds NO value for a masked binding, so the empty string is composed at the boundary from the absence. Addressed by the `bbUuid` the list publishes, so the id a caller walks to is the id this route accepts. A uuid this repository does not hold is Bitbucket's own 404
the step log has served reader.jobLogText(job) since #311; the row was still `generate` only because no conformance entry drove it
the build-container variant of the same log - the document defines log_uuid as the step UUID for the main build container, and those bytes are reader.jobLogText(job). This universe models no service containers, so any other log_uuid is a 404
SERVED 2026-09-12 (hello-19 wave W-a): `ci_binding` - the same canon GitHub serves as Actions secrets and variables, in Bitbucket's dialect. `secured` IS canon's `masked`, so the two hosts cannot disagree about which of this organization's settings may be published, and `value` is `""` on a secured row because the vendored property description says so in as many words ("If the variable is secured, this will be empty") - canon holds NO value for a masked binding, so the empty string is composed at the boundary from the absence. The legacy team path addresses the WORKSPACE account, which owns every repository in this universe, so it answers `/workspaces/{workspace}/pipelines-config/variables` BYTE-IDENTICALLY through the same closure - the ruling `/teams/{username}/search/code` already set on 2026-09-12, and conformance asserts the two agree rather than letting two paths for one account drift
SERVED 2026-09-12 (hello-19 wave W-a): `ci_binding` - the same canon GitHub serves as Actions secrets and variables, in Bitbucket's dialect. `secured` IS canon's `masked`, so the two hosts cannot disagree about which of this organization's settings may be published, and `value` is `""` on a secured row because the vendored property description says so in as many words ("If the variable is secured, this will be empty") - canon holds NO value for a masked binding, so the empty string is composed at the boundary from the absence. The by-uuid half of the same alias, through the same closure as the workspace route
SERVED 2026-09-12 (hello-19 wave W-a) as Bitbucket's empty page at 200: this path addresses a PERSON, and no person in this universe owns a repository or runs a pipeline of their own - so there is nothing for a person-scoped pipeline variable to configure. The WORKSPACE's variables are deliberately NOT served here; that is what the `/teams/{username}` alias is for, and serving one under the other's name is the confusion the hello-19 seed brief flagged (§G5). UNGATED across generations, for the same reason `/users/{selected_user}/search/code` is: repository ownership is a property of the model rather than of a feature flag
SERVED 2026-09-12 (hello-19 wave W-a): `ci_binding` - the same canon GitHub serves as Actions secrets and variables, in Bitbucket's dialect. `secured` IS canon's `masked`, so the two hosts cannot disagree about which of this organization's settings may be published, and `value` is `""` on a secured row because the vendored property description says so in as many words ("If the variable is secured, this will be empty") - canon holds NO value for a masked binding, so the empty string is composed at the boundary from the absence. The workspace account's own variables - canon's organization scope
SERVED 2026-09-12 (hello-19 wave W-a): `ci_binding` - the same canon GitHub serves as Actions secrets and variables, in Bitbucket's dialect. `secured` IS canon's `masked`, so the two hosts cannot disagree about which of this organization's settings may be published, and `value` is `""` on a secured row because the vendored property description says so in as many words ("If the variable is secured, this will be empty") - canon holds NO value for a masked binding, so the empty string is composed at the boundary from the absence. Addressed by the uuid the list publishes
generate — new canon entity ActionsPolicy
generate — new canon entity PipelineSshKey
generate — new canon entity PipelineSshKey
generate — new canon entity PipelineSshKey
generate — new canon entity ActionsCache
generate — new canon entity ActionsCache
generate — new canon entity Runner
generate — new canon entity Runner
derive — the report is the AGGREGATE of reader.testResultsForJob(job) (packages/artifact/src/reader.ts:2320): hello-15's canon.TestResult (packages/canon/src/ci-config.ts:204) carries a status and a duration per case, so the pass/fail/skip counts are a fold over rows that exist. The 2026-09-09 serving wave did NOT build this row, and the reason is a spec gap rather than canon: the vendored swagger declares a 200 with NO content block and NO test_* component for it, and Atlassian publishes no response body for it anywhere a capture could cite. Atlassian's own KB names three summary fields (number_of_test_cases, number_of_failed_test_cases, number_of_successful_test_cases, in a working jq example) and nothing more, so any further field would be invented — which is what invariant #4 forbids and what the DECISIONS §P17 Wave B `shape` pattern requires documented evidence to avoid. The row needs a CAPTURE, not a canon entity
derive — reader.listTestCases(repo) (packages/artifact/src/reader.ts:2308) joined to reader.testResultsForJob(job) (:2320): canon.TestCase (packages/canon/src/ci-config.ts:156) is the DEFINITION and TestResult is this job's run of it. The 2026-09-09 serving wave did NOT build this row, and the reason is a spec gap rather than canon: the vendored swagger declares a 200 with NO content block and NO test_* component for it, and Atlassian publishes no response body for it anywhere a capture could cite. Atlassian's own KB names three summary fields (number_of_test_cases, number_of_failed_test_cases, number_of_successful_test_cases, in a working jq example) and nothing more, so any further field would be invented — which is what invariant #4 forbids and what the DECISIONS §P17 Wave B `shape` pattern requires documented evidence to avoid. The row needs a CAPTURE, not a canon entity
derive — the failure text of one case in one job — reader.testResultsForJob(job) (packages/artifact/src/reader.ts:2320) filtered to the case; canon.TestResult.messageSlot (packages/canon/src/ci-config.ts:213) is the theme's per-status message, set exactly on failure and error. The 2026-09-09 serving wave did NOT build this row, and the reason is a spec gap rather than canon: the vendored swagger declares a 200 with NO content block and NO test_* component for it, and Atlassian publishes no response body for it anywhere a capture could cite. Atlassian's own KB names three summary fields (number_of_test_cases, number_of_failed_test_cases, number_of_successful_test_cases, in a working jq example) and nothing more, so any further field would be invented — which is what invariant #4 forbids and what the DECISIONS §P17 Wave B `shape` pattern requires documented evidence to avoid. The row needs a CAPTURE, not a canon entity
refuse — REFUSED 2026-09-12 (hello-19 wave W-a) with Bitbucket's own 404: no uuid resolves over the empty collection its list half answers - see that row
generate — new canon entity PipelineOidcConfig
generate — new canon entity PipelineOidcConfig
generate — new canon entity Runner
generate — new canon entity Runner
SERVED 2026-09-12 (hello-17): the approval rule's approvers, validated against `paginated_accounts`. hello-17 `reviewPolicy` (decisions/2026-09-11-0200) writes one approval rule per repository that has both merged pulls and an approver, and every field of it is a measurement: the approvers are the people with an `approve` review there, which is the same derivation the access pass uses to promote a plain member to repository `maintainer`, so the artifact holds ONE answer to "who may approve here" rather than two that can drift. Bitbucket's default reviewers are that rule read from the other end. THE ORDER IS THE RULE'S OWN — approvers is a comma-joined id list in canonical id order — and is deliberately NOT re-sorted by login, which would make the page order a function of the theme. Conformance re-derives the approver set from the review log and asserts every default reviewer really approved something in this repository. An artifact that predates the domain answers the hello-17 generation gap: an empty default-reviewer list would say this repository automatically requests nobody, which is a claim about how this team reviews rather than about the snapshot the caller pinned.
SERVED 2026-09-12 (hello-17): one default reviewer, addressed by the account UUID, the Atlassian account id or the nickname — the three spellings this host resolves users by, and the nickname because the parameter is literally named for a username and a client that read one off the list will send it. Somebody who is not on the rule is Bitbucket's own 404, which conformance drives. An artifact that predates the domain answers the hello-17 generation gap: an empty default-reviewer list would say this repository automatically requests nobody, which is a claim about how this team reviews rather than about the snapshot the caller pinned.
SERVED 2026-09-12 (hello-17): the repository's own reviewers plus whatever the project adds, each row saying WHICH — `default_reviewer_and_type`. `reviewer_type` is "repository" on every row and that is DERIVED, not defaulted: canon writes approval rules at repository scope only, because the subgroup owns no repository and so has no merge to measure a policy from. Conformance asserts the effective list is exactly the repository list, which is what "plus nothing from the project" means, and turns red the day a project-scoped rule exists. An artifact that predates the domain answers the hello-17 generation gap: an empty default-reviewer list would say this repository automatically requests nobody, which is a claim about how this team reviews rather than about the snapshot the caller pinned.
SERVED 2026-09-12 (coverage wave W3): the documented 302 to `/file-conflicts/{baseTip}..{head}`. The revspec names the two TIPS and never the merge base, which is the whole judgment on this row: a merge base conflicts with the head on nothing by construction, so a redirect naming it would always answer an empty page. The base tip is the merge commit's first parent on a merged pull and the base branch's head on an open one - the same instant reader.pullDiffRange resolves the base from
SERVED 2026-09-12 (coverage wave W3): the documented 302 to `/patch/{head}..{mergeBase}` - SOURCE first, which is the order the patch operation documents ('the first commit represents the source') and what makes the series the pull's own commits. Conformance follows the redirect and asserts the series carries exactly the commits `/pullrequests/{id}/commits` lists
refuse — this universe accepts no writes, so no merge was ever queued and no task id relates to this pull request - Bitbucket's own 400 for such a task id is the whole answer (the operation declares 400/403/409 and no 404, and its 400 description IS the ruled condition: "If the provided task ID does not relate to this pull request"; founder ruling 2026-09-03 on proposal 1 of coverage/reviews/2026-09-03-bitbucket-refuse-shapes.md, superseding the 404 read from the Jira async-job precedent)
generate — new canon entity PullTask
generate — new canon entity PullTask
SERVED 2026-09-09 (hello-16): the caller's own workspace membership — reader.listMemberships("org", org) (reader.ts:2961) narrowed to whoever GET /user resolves to. `owner` exactly when the role catalog gives that role `org:admin`, which is a rung the ARTIFACT carries rather than a table copied into the renderer; conformance recomputes it. `collaborator` is never produced, because nobody in this universe is outside the workspace — every person carries member = 1 — and Atlassian is removing the role from the API anyway. Conformance asserts this single row is byte-identical to /user/workspaces/{workspace}/permission
SERVED 2026-09-12 (coverage wave W3): the workspaces the caller can reach, with whether they administer each - reader.listMemberships('org', org) restricted to the viewer, `administrator` being `org:admin` read through the artifact's role catalog. The embedded object is `workspace_base`, whose links member declares self and avatar and forbids the rest, so the richer block condensedWorkspace carries is deliberately not used here. Behind the hello-16 membership gate for the reason `/user/permissions/workspaces` is: `administrator` is a PERMISSION, and `false` on an older pin would be a claim about the caller rather than about the snapshot
SERVED 2026-09-09 (hello-16): the caller's effective workspace role as one `workspace_membership` — reader.membershipFor("org", org, person) (reader.ts:2977), byte-identical to the single row /user/permissions/workspaces returns and asserted so. A workspace slug this host does not serve is the ordinary 404, checked separately from the generation gap so a typo never reads as a missing generation
SERVED 2026-09-12 (hello-18): Bitbucket's webhook_subscription over canon's webhook_subscription, which hello-18's features.webhookFilters widened from PagerDuty's three account scopes to the three a repository host publishes at — so a repository hook and a workspace hook are ONE object at two scopes, which is exactly why Bitbucket's own subject_type is an enum of those two words. THE EVENT NAMES ARE THIS RENDERER'S TRANSLATION, not canon's: canon's vocabulary is neutral because three renderers read that table, and Bitbucket spells the same six deliveries ref.pushed -> repo:push, change.opened -> pullrequest:created, change.merged -> pullrequest:fulfilled, issue.opened -> issue:created, issue.closed -> issue:updated and build.completed -> repo:commit_status_updated. Two of those needed an argument and both are recorded: Bitbucket publishes no `issue:closed` at all, and closing an issue IS an `issue:updated` in its vocabulary; and a build result on Bitbucket IS a commit status, reported by creating one when the build starts and UPDATING it when it finishes, which is the half canon records. Conformance re-derives the mapping independently of the renderer and asserts every served name is one the vendored enum publishes. This is the ACCOUNT-WIDE feed — a different canonical row from the repository's, and a different row again from PagerDuty's account-scoped subscription, which is why the widened vocabulary carries all three. `subject` is the condensed workspace this host serves at its own address, and conformance asserts the repository's hook is NOT listed here. `secret_set` is FALSE and is sent, because canon states there is no signing secret; `secret` itself is never sent, which is also what real Bitbucket does. Gated on hasWebhookFilters(); an older artifact answers the named hello-18 webhook gap rather than an empty envelope, because an empty envelope there would say this team wires up nothing
SERVED 2026-09-12 (hello-18): the same workspace hook by its braced uuid, byte-identical to the row the list publishes. A repository hook's uuid is Bitbucket's 404 here, which conformance drives from both sides. Gated on hasWebhookFilters()
SERVED 2026-09-12 (coverage wave W3): registered beside the green `/workspaces/{workspace}/members` and, like `/user`, never asserted until now: renderMembership over the person a uuid or login names. Conformance asserts the user and workspace it names are the artifact's own
SERVED 2026-09-09 (hello-16): the workspace roster with each member's effective role — reader.listMemberships("org", org) (reader.ts:2961) restricted to `active`, validated against `paginated_workspace_memberships`. The two PENDING invitations canon carries are excluded, and asserted excluded: an offer nobody accepted is not a member, and Bitbucket's own words are "the list of members in a workspace" (this host publishes no invitation surface, because Bitbucket Cloud has none). Conformance asserts the people here are exactly the people /workspaces/{workspace}/members lists, walks page/pagelen/next to the last page, and recomputes every permission from the role catalog
SERVED 2026-09-09 (hello-16): every (person, repository) EFFECTIVE permission in the workspace. Every member appears on every repository and that is not padding — canon's `member` role carries `repo:write` and this workspace owns every repository, so every member really can push to each one; a shorter list would be a different claim. `repo:maintain` collapses UP to Bitbucket's `admin` rather than to `write`, because branch restrictions and repository configuration — what canon's `maintainer` is FOR — are what Bitbucket's admin grants and its write does not; conformance asserts at least one row is therefore HIGHER than the person's org role alone gives, which is what makes canon's repository-scoped memberships visible on this host at all
SERVED 2026-09-09 (hello-16): the same effective permissions for ONE repository, through the same maximum. The repository is resolved by slug or braced UUID exactly as every other repository route on this host resolves it, so a slug that names none is the ordinary 404 rather than an empty page
SERVED 2026-09-12 (coverage wave W3): the ONE project every repository has advertised in its `project` member since wave A, expanded from condensedProject to the full shape. It is deliberately NOT canon's hello-16 `namespace`: repository.project is condensed-project on all 127 registered pins, so a list naming the namespace would not contain the project each repository says it belongs to, and `/projects/{key}/deploy-keys` - green since the hello-15 wave on the condensed key - would answer for a key the list omits. The namespace reaches GitLab as a SUBGROUP, the surface it was modelled for. `description`, `updated_on` and `is_private` are omitted because canon says nothing about any of them
SERVED 2026-09-12 (coverage wave W3): every pull request in the workspace one person authored - reader.listPulls() across the repositories, filtered by `pull.author`. Bitbucket's default is OPEN only and its `state` parameter is repeatable, the same grammar `/repositories/{ws}/{slug}/pullrequests` already implements and the same three words canon's states map onto. The author is addressed the way `/2.0/users/{selected_user}` addresses a person here; a member who authored nothing gets an honest empty page and a selected_user who is nobody gets Bitbucket's 404
SERVED 2026-09-09 (hello-16): the workspace's system signing key — the `gpg` credential canon puts on the MACHINE ACCOUNT, which the entity itself explains: "a workspace signing key is the ORG's key, not a person's, and the bot is the only principal in this universe that could hold an org-wide one" (packages/canon/src/credentials.ts). It is therefore served HERE and excluded from /users/{selected_user}/gpg-keys, so the workspace's key is never published as somebody's own — conformance asserts both halves. The vendored swagger declares a 200 with NO content block (a documented Bitbucket spec gap), and the operation's own description — "in most cases a single key is returned; during a key rotation period, two keys may be returned" — describes a COLLECTION, so it is this host's ordinary paginated envelope and the shape assertion is the gate. No armored block is served, for the reason /users/{selected_user}/gpg-keys states
no repository in this universe is a fork and none has been forked: the workspace owns every repository and there is no account outside it to fork into
SERVED 2026-09-12 (hello-18): Bitbucket's webhook_subscription over canon's webhook_subscription, which hello-18's features.webhookFilters widened from PagerDuty's three account scopes to the three a repository host publishes at — so a repository hook and a workspace hook are ONE object at two scopes, which is exactly why Bitbucket's own subject_type is an enum of those two words. THE EVENT NAMES ARE THIS RENDERER'S TRANSLATION, not canon's: canon's vocabulary is neutral because three renderers read that table, and Bitbucket spells the same six deliveries ref.pushed -> repo:push, change.opened -> pullrequest:created, change.merged -> pullrequest:fulfilled, issue.opened -> issue:created, issue.closed -> issue:updated and build.completed -> repo:commit_status_updated. Two of those needed an argument and both are recorded: Bitbucket publishes no `issue:closed` at all, and closing an issue IS an `issue:updated` in its vocabulary; and a build result on Bitbucket IS a commit status, reported by creating one when the build starts and UPDATING it when it finishes, which is the half canon records. Conformance re-derives the mapping independently of the renderer and asserts every served name is one the vendored enum publishes. `subject` is the condensed repository this host already serves at its own address, so a hook and the thing it is installed on cannot disagree (invariant #5), and conformance asserts the workspace's hook is NOT listed here. `secret_set` is FALSE and is sent, because canon states there is no signing secret; `secret` itself is never sent, which is also what real Bitbucket does. Gated on hasWebhookFilters(); an older artifact answers the named hello-18 webhook gap rather than an empty envelope, because an empty envelope there would say this team wires up nothing
SERVED 2026-09-12 (hello-18): the same hook by its braced uuid, byte-identical to the row the list publishes — conformance compares them directly. A WORKSPACE HOOK'S UUID IS A 404 HERE and a repository hook's is a 404 on the workspace path: the two scopes are different objects, and a host that resolved either id at either address would be publishing one subscription twice. Conformance drives both crossings. A uuid nobody holds is Bitbucket's own 404. Gated on hasWebhookFilters()
SERVED 2026-09-12 (coverage wave W3): `type` alone, which is the vendored schema's only required member and the whole of what this universe knows. `override_settings` enumerates the settings a repository has taken control of from its project; canon models no per-repository setting at all - every repository reports the same merge strategies and the same branch model because both are derived rather than stored - so there is nothing overridden to enumerate. Sending `{}` would claim the same thing less clearly and inventing booleans for settings canon does not carry is a placeholder
SERVED 2026-09-12 (hello-17): the EXPLICIT group permissions on this repository — reader.teamGrants("repo", repo), validated against `paginated_repository_group_permissions`. THIS ROW WAS RULED EMPTY BY THE FOUNDER ON 2026-09-10 on the claim that "canon's Membership binds a PERSON to a container and never a team to one … no team-to-repository grant exists in any generation", and that ruling said in as many words what would falsify it: the expectsEmpty entry fails "the day canon grants a team access to a container". hello-17 `grants` (decisions/2026-09-11-2304) lets a Membership name a TEAM as its principal: the team that owns the most components of a repository holds `maintainer` on it and a team that owns none but reviews it holds `member`, with ownership derived from the commit histogram. A Bitbucket GROUP is canon's TEAM on this host, so a group's explicit permission IS a team-principal grant. The permission comes through the same RoleVocabulary.repository map every other grant on this host goes through, so a team holding `maintainer` reports the same `admin` a person holding `maintainer` reports; conformance recomputes it from the role catalog and asserts no team-principal row leaks into /permissions-config/users and no person into this list. EVERY REGISTERED PIN ANSWERS EXACTLY AS IT DID BEFORE THIS WAVE. The route carries no generation gate: it reads reader.teamGrants()/reader.namespaceMembers(), which return [] on any artifact whose canon has no such row, so a -g11 pin goes on sending the same empty page and goes on saying something true about the universe it holds. What changed is the ARGUMENT — the emptiness was called a property of the MODEL and is now a property of each ARTIFACT — and packages/conformance/src/bitbucket/access.test.ts asserts the pin answer on hello-16, hello-15 and hello-4.
SERVED 2026-09-12 (hello-17): one group's explicit permission on this repository, addressed by the TEAM SLUG this host publishes everywhere else. Re-slugging the team name here would mint a second identity for one team and the two could then disagree, so `slug` is canon's own and `full_slug` is Bitbucket's `workspace:group` form built from the two slugs. hello-17 `grants` (decisions/2026-09-11-2304) lets a Membership name a TEAM as its principal: the team that owns the most components of a repository holds `maintainer` on it and a team that owns none but reviews it holds `member`, with ownership derived from the commit histogram. A Bitbucket GROUP is canon's TEAM on this host, so a group's explicit permission IS a team-principal grant. A slug that names no team, and a team that holds no grant on this repository, are both Bitbucket's own 404 — the rule the user twin follows, because `none` is what a WRITE removes a grant with and not what a read returns for somebody who holds nothing. Conformance drives both negatives. Answers the honest 404 on an artifact with no team grant, because the parent list is empty there and no group_slug resolves
SERVED 2026-09-09 (hello-16): the EXPLICIT grants on this repository — reader.listMemberships("repo", repo) (packages/artifact/src/reader.ts:2961), validated against `paginated_repository_user_permissions`. Canon writes a repository-scoped membership ONLY where the repository's roster genuinely differs from the organization's (rule 5, packages/generator/src/access.ts), which is exactly what Bitbucket means by "explicit" — so the list is short and every row in it says something the workspace roster does not. Canon's five role keys reach Bitbucket's `admin`/`write`/`read` through ONE map (packages/renderer-bitbucket/src/render-access.ts) read off the role catalog's own `repo:` rung rather than a hand-written table; conformance recomputes it and asserts this list is a strict SUBSET of the effective one
SERVED 2026-09-09 (hello-16): one explicit grant, addressed by the account UUID or Atlassian account id this host already resolves users by — reader.membershipFor("repo", repo, person) (reader.ts:2977). A person with no explicit grant on this repository is Bitbucket's own 404 rather than a `permission: "none"` row: `none` is what a WRITE removes a grant with, and their effective permission is still published by /workspaces/{workspace}/permissions/repositories/{repo_slug}. Conformance asserts both halves against the same artifact
SERVED 2026-09-09 (hello-16): the caller's EFFECTIVE permission on each repository — "the highest level of permission the user has … does not distinguish between direct and indirect (group) privileges", which is the maximum of the rung their org role gives and the rung any direct repository membership gives, taken in ONE place (packages/renderer-bitbucket/src/render-access.ts) off reader.listMemberships (reader.ts:2961). Bitbucket marks this operation DEPRECATED in favour of the workspace-scoped one; both are served and conformance asserts their `values` are byte-identical, because this workspace owns every repository the artifact carries
SERVED 2026-09-09 (hello-16): the workspace-scoped replacement Bitbucket points the deprecated /user/permissions/repositories at, and the same effective permissions — this workspace owns every repository the artifact carries, which is a fact about the universe rather than a shortcut: the resolution still goes through the workspace, so a slug that names none 404s. Conformance asserts the two bodies agree
generate — new canon entity RepoSubscription
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity Snippet
generate — new canon entity RepoSubscription
generate — new canon entity RepoSubscription
the same fact as the GitHub row, on the third git host: canon carries no commit comments. The commit is resolved first, so a bad SHA still 404s and a client can tell 'no comments' from 'no such commit' (DECISIONS 2026-08-20 [commit-detail] decision 2)
SERVED 2026-09-12 (coverage wave W3): the paths BOTH sides changed since their merge base, to different content - reader.treeFiles() over the same git blobs `/diff/{spec}` reads. A path one side left alone merges cleanly and a path both sides changed identically merges cleanly, so neither is reported, and `scenario` is delete_modify/modify_delete/content read off which side dropped the file. `message` is omitted: Bitbucket fills it from its merge engine and this host runs none. The 200 is a page envelope with no required member, so a spec whose sides merge cleanly is an honest empty page rather than an invented conflict
SERVED 2026-09-12 (coverage wave W3): reader.mergeBase(a,b) over the two commits the revspec names, rendered by the same renderCommit `/commit/{commit}` is green for - conformance asserts the two bodies are byte-identical and that the answer is an ancestor of both sides. A revspec naming ONE commit is Bitbucket's own documented 404: a merge base is a question about two sides, and one commit does not pose it
SERVED 2026-09-12 (coverage wave W3): a git format-patch series, raw text - the vendored operation declares a 200 with no content at all, so every line is asserted instead: the `From <sha>` lines are reader.commitsOnlyIn(source,destination) in git's own oldest-first order, the `From:`/`Date:`/`Subject:` headers are the commit's own author and instant, and each patch body IS the text `/diff/{sha}` serves for that commit. `Mon Sep 17 00:00:00 2001` is git's fixed mbox sentinel rather than a date, and the `-- <git version>` trailer is omitted because no git produced this patch
refuse — the id half of an empty this file already ruled and a test already arms: canon carries no commit comments (DECISIONS 2026-08-20 [commit-detail] decision 2, the `mode: empty` row directly above this one), so no comment id can resolve and Bitbucket's own 404 is the only possible answer
SERVED 2026-09-09: reader.listCheckoutKeys(repo) (packages/artifact/src/reader.ts:2351). canon.CheckoutKeyType is literally "deploy-key" (packages/canon/src/ci-config.ts:221); the served `key` and `comment` reassemble CheckoutKeyRow.public_key exactly, and the conformance entry base64-decodes the blob and SHA-256s it back to the artifact's own fingerprint_sha256 — which is what separates a key from a string shaped like one. `last_used` is omitted because canon records no use of any key
SERVED 2026-09-09: reader.listCheckoutKeys(repo) (packages/artifact/src/reader.ts:2351). canon.CheckoutKeyType is literally "deploy-key" (packages/canon/src/ci-config.ts:221); the served `key` and `comment` reassemble CheckoutKeyRow.public_key exactly, and the conformance entry base64-decodes the blob and SHA-256s it back to the artifact's own fingerprint_sha256 — which is what separates a key from a string shaped like one. `last_used` is omitted because canon records no use of any key. Addressed by the numeric key_id the spec's own examples use, derived through bbInt like every other numeric Bitbucket id
SERVED 2026-09-09: the PROJECT-scoped view of the same rows — reader.listCheckoutKeys(repo) over the repositories this workspace's project holds, as `project_deploy_key` (which names its project and, unlike the repository shape, carries no `repository`). The project key is condensedProject's, the same value every repository object has advertised since Wave A, so a caller who read `project.key` off a repository can address this route with it. Answers the hello-15 generation gap on an older artifact
SERVED 2026-09-09: the same project-scoped key by its numeric key_id, byte-identical to the row the project list serves. A project key this workspace does not hold is Bitbucket's own 404
SERVED 2026-09-12 (coverage wave W3): the single-object twin of the list above, resolved by the key every repository publishes (the projectCtx `/projects/{key}/deploy-keys` already uses). Conformance asserts the by-key object is the listed object and that an unknown key is still Bitbucket's 404
THE PROJECT HAS NO DEFAULT REVIEWERS, because canon writes no project-scoped approval rule. hello-17 `reviewPolicy` (decisions/2026-09-11-0200) writes one approval rule per repository that has both merged pulls and an approver, and every field of it is a measurement: the approvers are the people with an `approve` review there, which is the same derivation the access pass uses to promote a plain member to repository `maintainer`, so the artifact holds ONE answer to "who may approve here" rather than two that can drift. Bitbucket's default reviewers are that rule read from the other end. A Bitbucket project is canon's `namespace`, the one namespace owns no repository (founder ruling F1, hello-16) and therefore has no merge to measure a policy from, so reader.listApprovalRules("namespace", ns) is empty and Bitbucket's own answer is the paginated empty page at 200. READ rather than hard-coded: the query is the thing that has to say so. Falsifiable: the conformance entry asserts a REPOSITORY rule exists (so the empty is not vacuous) and that the namespace has none, and fails the day the subgroup owns a repository. Served behind the hello-17 generation gate for the reason the sibling rows state — an empty list on an older pin would be a claim about how that team reviews. RULED empty by the hello-17 bitbucket wave, 2026-09-12
NO TEAM HOLDS A GRANT ON THE WORKSPACE'S PROJECT, and this is the NARROWER reason that replaces the 2026-09-10 ruling. That ruling gave two grounds — canon binds a person to a container and never a team to one, and the project (canon's `namespace`) carries no membership row — and hello-17's `grants` makes BOTH false: a team can be a principal, and the subgroup has a roster. What is still true is where canon writes team grants: the rule measures component ownership per REPOSITORY, and the subgroup owns no repository (founder ruling F1, hello-16), so reader.teamGrants("namespace", ns) is genuinely empty. Bitbucket's own answer is the paginated empty page at 200, and it is READ rather than hard-coded — the query is the thing that has to say so. Falsifiable from the side that is left: the conformance entry asserts team grants EXIST in this universe (so the empty is not vacuous) and that none of them scopes to the namespace, and fails the day one does. RULED empty by the hello-17 bitbucket wave, 2026-09-12, superseding the 2026-09-10 ruling on this row
SERVED 2026-09-12 (hello-17): the project's own roster — reader.namespaceMembers(ns), validated against `paginated_project_user_permissions`. THIS ROW WAS RULED EMPTY BY THE FOUNDER ON 2026-09-10 because the one namespace "carries no membership row", and that ruling named its own falsifier: the expectsEmpty entry fails "the day a namespace gets a member". hello-17's `grants` gives it five — the org owner, the org admins and each team's lead, the people whose authority is over the PLAN rather than over the code — which is five of seventeen, and conformance asserts the project roster is a PROPER subset of the workspace's, because a list that copied the workspace roster would make this operation indistinguishable from /workspaces/{workspace}/permissions. Bitbucket's `create-repo` rung is never produced: canon's MEMBERSHIP_PERMISSIONS has no repository-creation level at all, so emitting it would hand somebody a grant nothing in this universe made. EVERY REGISTERED PIN ANSWERS EXACTLY AS IT DID BEFORE THIS WAVE. The route carries no generation gate: it reads reader.teamGrants()/reader.namespaceMembers(), which return [] on any artifact whose canon has no such row, so a -g11 pin goes on sending the same empty page and goes on saying something true about the universe it holds. What changed is the ARGUMENT — the emptiness was called a property of the MODEL and is now a property of each ARTIFACT — and packages/conformance/src/bitbucket/access.test.ts asserts the pin answer on hello-16, hello-15 and hello-4.
SERVED 2026-09-12 (hello-17): one person's explicit permission on the project, addressed by the account UUID or Atlassian account id this host already resolves users by — reader.namespaceMembers(ns) narrowed to that person. THE hello-16 ASK IS ANSWERED: that wave said the row needed "a namespace-scoped membership, the same canon the GitLab subgroup wave (#431) left open", and hello-17's `grants` writes five. A person who is not on the subgroup's roster is Bitbucket's own 404 rather than a `permission: "none"` row; their workspace standing is still published by /workspaces/{workspace}/permissions, and conformance drives both. Answers the honest 404 on an artifact whose namespace has no member
generate — hello-17's `reviewPolicy` IS in canon and the three REPOSITORY rows went `derive` on 2026-09-12 — but this operation addresses one reviewer of a PROJECT-scoped rule, and canon writes none: a Bitbucket project is canon's `namespace`, the one namespace owns no repository (founder ruling F1) and so has no merge to measure a policy from. The parent list is an honest empty, no `selected_user` can ever return 200, and there is nothing for conformance to validate — so the row answers the honest coverage 404 rather than being `empty` itself. THE ASK: an approval rule at namespace scope, which needs the subgroup to own a repository. Reported by the 2026-09-12 bitbucket hello-17 serving wave
generate — hello-17's `grants` IS in canon and the REPOSITORY twin went `derive` on 2026-09-12 — but this operation addresses ONE group's permission on the PROJECT, and the project's group list is still empty: canon writes team-principal grants at repository scope only, because the rule measures component ownership and the subgroup owns no repository (founder ruling F1). No `group_slug` can ever return 200, so there is nothing for conformance to validate and the row answers the honest coverage 404 rather than being `empty` itself — a single-object operation has no empty collection to serve. THE ASK: a team-to-container grant at NAMESPACE scope, which needs the subgroup to own a repository. Reported by the 2026-09-12 bitbucket hello-17 serving wave
generate — new canon entity BranchingModel
generate — new canon entity BranchingModel
generate — new canon entity BranchingModel
generate — new canon entity BranchingModel
generate — new canon entity BranchingModel
refuse — no Atlassian Connect app is installed on this workspace; these routes are addressed to an installed app's own key scope and Bitbucket answers 404 to a caller that is not that app (the ruled Jira Connect/Forge precedent, coverage/reviews/2026-09-01-jira-modes.md row 1)
refuse — no Atlassian Connect app is installed on this workspace; these routes are addressed to an installed app's own key scope and Bitbucket answers 404 to a caller that is not that app (the ruled Jira Connect/Forge precedent, coverage/reviews/2026-09-01-jira-modes.md row 1)
refuse — no Atlassian Connect app is installed on this workspace; these routes are addressed to an installed app's own key scope and Bitbucket answers 404 to a caller that is not that app (the ruled Jira Connect/Forge precedent, coverage/reviews/2026-09-01-jira-modes.md row 1)
refuse — no Atlassian Connect app is installed on this workspace; these routes are addressed to an installed app's own key scope and Bitbucket answers 404 to a caller that is not that app (the ruled Jira Connect/Forge precedent, coverage/reviews/2026-09-01-jira-modes.md row 1)
generate — new canon entity SecurityFinding
generate — new canon entity SecurityFinding
generate — new canon entity SecurityFinding
generate — new canon entity SecurityFinding
SERVED 2026-09-09 (hello-16): a person's ACCESS keys — reader.credentialsForPerson(user, "ssh-auth") (reader.ts:3016). Canon's `ssh-signing` rows are deliberately excluded and this host publishes them nowhere: a key authorised to push and a key trusted to attest authorship are different grants (packages/canon/src/credentials.ts), Bitbucket Cloud has one account SSH surface and it is the ACCESS one, and serving a signing key here would assert a grant nobody made. The served `key` and `comment` are a SPLIT of the artifact's single OpenSSH line, so they can never describe different bytes, and conformance re-hashes the served line and asserts it reproduces the artifact's own `fingerprint_sha256` — the check a client runs with `ssh-keygen -lf`
SERVED 2026-09-09 (hello-16): one access key by its UUID (the spec's own words for `key_id`: "the SSH key's UUID value"), byte-identical to the row the list serves. A signing key's UUID does NOT address it, which is the same exclusion the list makes and is asserted from both sides
SERVED 2026-09-09 (hello-16): a person's PERSONAL OpenPGP keys — reader.credentialsForPerson(user, "gpg") (reader.ts:3016). `key_id` and `fingerprint` are the artifact's own, and conformance RECOMPUTES the fingerprint from the packet the way GnuPG does (SHA-1 over 0x99‖len‖body) and checks the key id is its last eight bytes. The armored `key` is NOT served: a transferable OpenPGP key needs a user id and a self-signature, a self-signature needs a private half this universe never generated, and the field is opt-in on Bitbucket's own operation — #430's `raw_key: null` reasoning in Bitbucket's spelling, asserted ABSENT rather than left to be noticed. The WORKSPACE key is excluded here and served once at /workspaces/{workspace}/settings/gpg/public-key. `deviation` rather than green: the vendored schema types `parent_fingerprint` `string` while its own description says the value is null unless the key is a subkey, and every key here is a primary key
SERVED 2026-09-09 (hello-16): one personal GPG key by its v4 fingerprint, byte-identical to the row the list serves. A fingerprint this person does not hold is Bitbucket's own 404. `deviation` for the same vendored `parent_fingerprint` contradiction as the list
SERVED 2026-09-12 (coverage wave W3): the route was REGISTERED in wave A and never asserted, which is the whole of this row: renderUser(viewer()), the workspace's founding member. Conformance now asserts it is that person, that `/2.0/users/{uuid}` renders the identical account, and that `/2.0/user/emails` carries the same person's address - the caller-scoped surfaces on this host all report one identity or two of them describe two accounts
SERVED 2026-09-09 (hello-16): the authenticated caller's addresses — reader.credentialsForPerson(viewer, "email") (packages/artifact/src/reader.ts:3016). The caller is whoever GET /user already resolves to, read from ONE function so this host cannot disagree with itself about whose addresses these are. `is_confirmed` is the credential's own state rather than a decision, and exactly one address is primary — conformance asserts both and follows every `links.self` back to the single-address row. The vendored swagger declares NO 200 for this operation (only a `default` error), a documented Bitbucket spec gap, so the entry asserts the documented live shape plus canon consistency instead of a schema
SERVED 2026-09-09 (hello-16): one of the caller's addresses by its text, byte-identical to the row the list serves — conformance follows `links.self` from every list row and compares. An address the caller does not hold is Bitbucket's own 404. Same spec gap as the list: no 200 is declared, so the shape assertion is the gate
SERVED 2026-09-12 (coverage wave W3): the legacy team path addresses the WORKSPACE account, which owns every repository in this universe, so it answers `/workspaces/{workspace}/search/code` byte-identically - conformance asserts exactly that, rather than letting two paths for one account drift
no person in this organization owns a repository — the workspace owns every one of them, which is what /workspaces/{workspace}/search/code searches; Bitbucket's own answer for an account with no matching code is the search page with `size: 0`. THE PERSON STILL RESOLVES and the query is still required: a selected_user nobody has is Bitbucket's 404 rather than an empty page, and a request with no `search_query` is still the documented 400 — an honest empty answers the question that was asked, and a request with no query asked none. Conformance asserts the WORKSPACE search over the same term is NOT empty, so the emptiness is about who owns the repositories rather than about the query, and re-reads every repository's own owner to check the fact the ruling rests on. UNGATED: repository ownership is a property of the model — renderOwner is the workspace on every repository object in every generation — so all 127 registered pins answer this same empty page. RULED by the founder 2026-09-12 (round 20)
SERVED 2026-09-12 (coverage wave W3): a case-insensitive substring match over the same git blobs `src` serves - the honest thing a replica holding the real files can say, and one a caller can reproduce by fetching them. Results are repository-major then path because Bitbucket ranks by relevance and this host cannot, so the same query always paginates identically. `query_substituted` is a knowable false: an unimplemented modifier is an explicit 400 rather than a silent rewrite, and only `repo:`, `project:` and `ext:` are implemented. `content_matches` carries one entry per matching LINE, with segments that reassemble the line exactly, because this host has no context window to claim around a match
generate — new canon entity BranchRestriction
generate — new canon entity BranchRestriction
hello-13's Download canon: the newest release publishes a tarball and a .sha256sums, so the download area lists real files with real sizes and download counts
the operation the document titles Get a download artifact link, whose only documented answer is a 302; we redirect to the same links.download href the list publishes, and never fabricate file bytes
derive
derive
refuse — no Atlassian Connect app is installed on this workspace; these routes are addressed to an installed app's own key scope and Bitbucket answers 404 to a caller that is not that app (the ruled Jira Connect/Forge precedent, coverage/reviews/2026-09-01-jira-modes.md row 1)
03 / What's simulated
Every Bitbucket call resolves against the same simulated data set every other provider serves. Counted from the bb-v2-g11 artifact (universe generation 11):
Each line below is one assertion in the conformance suite named beside it — run on every build, over one artifact, through the real renderers. Nothing is claimed here that no expect checks.
Every pull request's `source.branch.name` is the same string GitHub serves as `head.ref`, GitLab as `source_branch` and Azure DevOps as `refs/heads/…`.
Against GitHub, GitLab, Azure DevOps · parity/branch-parity.test.ts
For one commit sha, the pull request ids it is attributed to match GitHub's, each pipeline step matches a GitHub check run by name, `SUCCESSFUL`/`FAILED` matches GitHub's `success`/`failure` per job, and the PR diffstat's per-file added/deleted counts equal GitHub's compare.
Against GitHub · parity/commit-detail-parity.test.ts
The pipeline that ran on a commit here is the CircleCI pipeline for the same 40-character SHA, and the two hosts name the same jobs with the same verdicts.
Against CircleCI · parity/ci-parity.test.ts
A pipeline here and a Buildkite build are the same CI run — the same build number at the same commit SHA — and the step's log is BYTE-IDENTICAL to the job log Buildkite serves for it, because neither host builds the text.
Against Buildkite · parity/ci-log-parity.test.ts
04 / Known deviations
These answer with real data and match what Bitbucket actually returns, but fail the spec we vendor from Bitbucket — nearly always a bug in that published description. They are listed rather than counted so you can see whether one of them is the endpoint your client calls, and they are never counted as verified.
GET /users/{selected_user}/gpg-keysssh-gpgSERVED 2026-09-09 (hello-16): a person's PERSONAL OpenPGP keys — reader.credentialsForPerson(user, "gpg") (reader.ts:3016). `key_id` and `fingerprint` are the artifact's own, and conformance RECOMPUTES the fingerprint from the packet the way GnuPG does (SHA-1 over 0x99‖len‖body) and checks the key id is its last eight bytes. The armored `key` is NOT served: a transferable OpenPGP key needs a user id and a self-signature, a self-signature needs a private half this universe never generated, and the field is opt-in on Bitbucket's own operation — #430's `raw_key: null` reasoning in Bitbucket's spelling, asserted ABSENT rather than left to be noticed. The WORKSPACE key is excluded here and served once at /workspaces/{workspace}/settings/gpg/public-key. `deviation` rather than green: the vendored schema types `parent_fingerprint` `string` while its own description says the value is null unless the key is a subkey, and every key here is a primary keyGET /users/{selected_user}/gpg-keys/{fingerprint}ssh-gpgSERVED 2026-09-09 (hello-16): one personal GPG key by its v4 fingerprint, byte-identical to the row the list serves. A fingerprint this person does not hold is Bitbucket's own 404. `deviation` for the same vendored `parent_fingerprint` contradiction as the listGET /repositories/{workspace}/{repo_slug}/filehistory/{commit}/{path}source05 / Pinned snapshots
Each pin regenerates byte-identically on every request, so a test written against one never drifts. Generations are DIFFERENT universes, not versions of one — never swap a suffix expecting the same data.
bb-v2.snap.sandboxapis.devgeneration 12.0not in this generationbb-v2-g10.snap.sandboxapis.devgeneration 102.0servedbb-v2-g11.snap.sandboxapis.devgeneration 112.0servedbb-v2-g2.snap.sandboxapis.devgeneration 22.0servedbb-v2-g3.snap.sandboxapis.devgeneration 32.0servedbb-v2-g5.snap.sandboxapis.devgeneration 52.0servedbb-v2-g6.snap.sandboxapis.devgeneration 62.0servedbb-v2-g8.snap.sandboxapis.devgeneration 82.0servedbb-v2-g9.snap.sandboxapis.devgeneration 92.0servedNeed an endpoint that is not served yet?
Every row Bitbucket's manifest carries is on this page, so “not here” is an answer rather than a gap in the rendering. Tell us which path and which client, and it moves up the queue — the order is set by what people ask for.