Skip to content

feat(platform-objects): sys_organization's data door admits update, column-gated by the ADR-0092 D2 whitelist - #16687

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-15873-organization-update-door
Sep 7, 2026
Merged

feat(platform-objects): sys_organization's data door admits update, column-gated by the ADR-0092 D2 whitelist#16687
os-musk merged 5 commits into
mainfrom
claude/issue-15873-organization-update-door

Conversation

@os-musk

@os-musk os-musk commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15873

Clause-②: yes. enable.apiMethods is the accept set of a published door — packages/rest/src/rest-server.ts (apiAccessDecision, called by enforceApiAccess) refuses an unlisted operation with 405 OBJECT_API_METHOD_NOT_ALLOWED before the engine is reached — and the value ships in packages/platform-objects/dist/identity/index.js / index.mjs. This PR changes what PATCH /api/v1/data/sys_organization/:id accepts on a published identity object, so it carries needs:contract-review (the card already does) and ⛔ does not go ready, is not enqueued and gets no auto-merge until the in-seat contract review returns.

The ruling this implements

Maintainer ruling 2026-09-07, decision batch #64, option (a) (issue comment 5564369547; maintainer reply, verbatim: 「同意」):

admit update in sys_organization.apiMethods and let the D2 whitelist do the column gating — the whitelist already exists for exactly this, and better-auth's own columns stay stripped

Options (b) and (c) were not taken. The 12:08Z re-presentation of A/B/C on the card (5570393087) was the PM seat's own duplicate of a decided question, reversed by the director (5571916300) and withdrawn (5572039366); nothing here re-opens it.

What changed

  • packages/platform-objects/src/identity/sys-organization.object.ts — the object's own declaration, nothing in the :1699 gate:
    • enable.apiMethods: ['get', 'list']['get', 'list', 'update']. create / delete still answer 405; bulk is not granted. ⚠️ The verb is not the whole accept set — see The accept set below: update also derives the update-mode import door, and the affordance flips /auth/me/permissions.
    • userActions: { edit: true } — required, not optional: a managedBy object runs through reconcileManagedApiMethods (objectql registry, ADR-0092 / ADR-0103 D3), which strips any write verb the resolved affordances do not grant and only warns. Without this line update would be declared in source and the door would keep answering 405 — the second silent gate api-key-ui-lifecycle: API keys cannot be revoked through any product route (405) — object declares revoke/restore as PATCH but disables the PATCH method #7727 measured on sys_api_key. The repo-wide sweep managed-api-method-affordance-sweep.test.ts and the object's own pin both hold it.
    • name, slug, logo, metadatareadonly: true. ADR-0092 D4's form-rendering constraint ("non-whitelisted fields must render non-editable in the standard edit form") — the same treatment sys_user (email, role, …) and sys_api_key (name, key, …) carry. UX only: the server boundary is the guard. The engine's static-readonly strip exempts system-context writers and better-auth's adapter stamps context.isSystem: true, so better-auth's organization/update still writes these columns (the sys_user.email precedent).
    • The managedBy is not enforced: generic CRUD bypasses better-auth on sys_team (data-integrity / security) #1591 comment is rewritten per the ruling: its "writes owned by better-auth" reasoning holds for better-auth's columns, which the whitelist still protects; it never covered the platform-owned extension columns.
  • packages/spec/src/data/api-methods-batch-conformance.test.tssys_organization added to SINGLE_RECORD_WRITE_ONLY with its reason. The ledger forces a choice between granting bulk and recording why not; the ruling widened one verb, and bulk (the /batch and *Many routes on an identity table) is a further widening it did not take. The stale-entry check refuses bulk being added later without retiring the row.
  • .changeset/organization-update-door.md@objectstack/platform-objects: minor, stating the widening and the held column gate.
  • Three pins (below).

Untouched: packages/plugins/plugin-auth/src/managed-extension-fields.ts (the whitelist is exactly the four ruled columns, unchanged), packages/rest/src/rest-server.ts, every ADR.

The accept set — three published surfaces move, all column-clamped by the same guard (contract-review patch round)

The first round named only the first. The review measured the other two on the real door; this round names them, pins them, and states them in the changeset.

  1. The data door. PATCH /api/v1/data/sys_organization/:id is admitted (apiMethods gains update, userActions.edit keeps it through registration). create / delete stay 405; bulk (/batch, *Many) is not granted.
  2. The derived import door. packages/spec/src/data/api-derivation.ts API_METHOD_DERIVATION: import: { any: ['create', 'update'] } — granting update derives import, so POST /api/v1/data/sys_organization/import (and the async /import/jobs route) is admitted in writeMode: 'update'. One request updates N rows; the import runner writes each row under the caller's context, so the D2 guard clamps every row. Insert / upsert modes stay 405 and the conjunct the envelope names is create. The door's own 405 envelope already advertised it in allowed.
  3. /auth/me/permissions. packages/plugins/plugin-hono-server/src/current-user-endpoints.ts: clampManagedObjectWrites reads userActions.edit for the better-auth bucket, annotateEffectiveApiOperations reports the effective set — for a principal the permission layer already admits, sys_organization.allowEdit goes false → true and apiOperations gains update and import. This is the payload the console renders its edit affordance from. organization_admin / member_default stay hard-denied on every better-auth table by managed-object-write-denies.ts (unchanged; the review's reading, worth the director's attention for the objectui follow-up: the product path this PR opens is the platform-admin path).

The SINGLE_RECORD_WRITE_ONLY row now says what is true: bulk is not granted; the derived update-mode import door is, column-clamped per row — the ledger is about bulk (/batch, *Many), which import does not read, so the exemption stands beside it.

Pins for 2 and 3, on the real door (organization-update-door.dogfood.test.ts, measured):

request result
GET /auth/me/permissionsobjects.sys_organization allowEdit: true, allowCreate: false, allowDelete: false; apiOperations contains update and import, not create / delete / bulk. Control: objects.sys_member (better-auth, no userActions) stays allowEdit: false for the same principal — the clamp is live, not a wildcard fold
POST /data/sys_organization/import { writeMode: 'update', matchFields: ['id'], rows: [{ id, name, timezone: 'Asia/Tokyo' }] } 200, updated: 1, errors: 0; read-back timezone === 'Asia/Tokyo', name unchanged
same, treatAsHistorical: true, row carrying name only 200 (per-row outcome report), updated: 0, results[0].ok === false, results[0].code === 'PERMISSION_DENIED'; name unchanged — no elevation
writeMode: 'insert', rows: [{ name, slug }] 405 OBJECT_API_METHOD_NOT_ALLOWED, error names 'create'; allowed contains update and import, not create

Non-vacuity, measured (second ablation, this round; prediction written first in the script header). Cut: add name (plus an inert unique marker) to MANAGED_EXTENSION_EDITABLE_FIELDS.sys_organization — widen the column gate itself — rebuild plugin-auth (dogfood consumes it through dist/), ablation-dist-preflight.mjs @objectstack/plugin-auth ablation_15873_marker ✓ present in dist/index.js + index.mjs. Predicted 4 of 11 door pins red; observed 2 of 11: the name-only PATCH transition pin (expected 200 to be 403) and the per-row import refusal pin (expected 1 to be +0) — the two guard-specific controls — went red; the "name unchanged" halves of the smuggle and import-update pins stayed green, because name is readonly (D4) and the engine's static-readonly strip, which runs after the guard for non-system callers, holds it too. Two layers, one observable; the pins now say so in their comments, and the per-row refusal is documented as the guard's own control on the import path. plugin-auth's column-gate pin: 5 of 21 red (whitelist equality, name-alone refusal, mixed payload, lifecycle stamps, D4 partition), all predicted-direction. Restore: git checkout HEAD -- with the absolute path → git hash-object = 802551b6a1cbb599b6076611b21472e4f4bd57b2 = HEAD: blob, path clean, rebuild, preflight --absent ✓ (marker absent from all 12 built files; its TREE check named three dirty paths — this round's own uncommitted edits, since committed, not residue), dogfood 11/11 and plugin-auth 21/21 green again.

Instrument note (from the review, adopted). reconcileManagedApiMethods runs at registration, not at build, so a property-read of dist/ cannot see anything it touches — with userActions removed, dist still reads ["get","list","update"] while the registered schema reads ["get","list"]. Which instrument each claim here rests on: apiMethods / userActions / readonly ship in the artifact — dist/ property-read; update survives registration — the registered schema (engine.getSchema, pinned in the dogfood file) and the affordance predicate (platform-objects pin); the two surfaces above — the real door (/auth/me/permissions response, the import route's response) and the guard's registered whitelist (plugin-auth pin, ablation 2).

The two things that decide whether this PR is right

1. The METHOD gate widened; the COLUMN gate held — proven on the real door, both directions

packages/qa/dogfood/test/organization-update-door.dogfood.test.ts boots the showcase stack, signs in as the seeded platform admin, mints an organization through better-auth's organization/create (the create_organization row action's target) and drives PATCH /api/v1/data/sys_organization/:id under that user context:

request before this PR after this PR (measured)
{ name } (better-auth column, alone) 405 OBJECT_API_METHOD_NOT_ALLOWED 403 PERMISSION_DENIED — the guard's own verdict; name unchanged on read-back
{ timezone: 'Asia/Shanghai', sort_order: 7 } 405 200, both values read back
{ require_mfa } (alone, at its current value) 405 200 — a non-whitelisted key sent alone is refused 403, so a 200 proves admission
{ sort_order: 9, name, slug, logo, metadata } (smuggled) 405 200; sort_order lands, the four better-auth columns read back unchanged; the guard logs stripped non-whitelisted field(s) … name, slug, logo, metadata (ADR-0092)
{ timezone: 'Mars/Olympus' } 405 400 VALIDATION_FAILED#14238's valueDomain: 'iana_time_zone' now judges the door
POST /data/sys_organization, DELETE /data/sys_organization/:id 405 405 OBJECT_API_METHOD_NOT_ALLOWED (unchanged)

Plus the REGISTERED schema (post-reconcile) contains update, not create / delete / bulk, with userActions.edit === true; and update_organization still targets /api/v1/auth/organization/update with name / slug / logo.

The transition is pinned, not only the after-state. The first row asserts { status, code } is not { 405, OBJECT_API_METHOD_NOT_ALLOWED } and then asserts 403 + PERMISSION_DENIED. A pin that could not tell the two refusals apart would not prove the method gate moved; a later change turning the refusal into a 200 or a 500 fails both halves.

Column gate at the unit level — packages/plugins/plugin-auth/src/sys-organization-update-door.test.ts drives the guard's beforeUpdate with the SHIPPED whitelist (registered from managedExtensionEditableFields('sys_organization'), not re-spelled): each of name / slug / logo / metadata alone → thrown error with code: 'PERMISSION_DENIED', status: 403; each of the four ruled columns alone passes un-stripped; a mixed payload is reduced to the whitelisted key; lifecycle stamps pass through but do not count. It also derives the D4 partition from the whitelist: every non-system column is either whitelisted or readonly, never both, never neither.

require_mfa is written at its current value on the door rather than flipped: require_mfa: true on the caller's own organization is enforced at the session-validation gate and would turn every later assertion into an MFA challenge. The unit pin flips it.

One D2 semantic worth stating plainly, because the ruling's execution note says a write of name "must be refused by the guard (403 with the D2 code), not silently ignored": sent ALONE it is refused 403; sent BESIDE a whitelisted column it is stripped and logged at warn, and the whitelisted column lands. That is ADR-0092 D2 as accepted ("Non-whitelisted keys are stripped; if the payload becomes empty … throws"), identical to sys_user and sys_api_key, and the reason D4's readonly flags matter: they keep a standard form from ever composing that mixed payload from an edit the user believes they made.

2. What this repo cannot answer about the Setup app — NOT MEASURABLE HERE

Ruling (a) rests on administrators setting these columns through the product. Measured in this tree (f0b8c0d363):

  • packages/platform-objects/src/apps/setup-nav.contributions.tsnav_organization mounts the ACTIVE organization's record page (objectName: 'sys_organization', recordId: '{current_org_id}') in the Setup app for every deployment, and nav_organizations the list, gated on org-scoping.
  • packages/platform-objects/src/pages/sys-organization.page.tsSysOrganizationDetailPage, a slotted record page that overrides only the tabs slot; its own comment says header, actions and details "fall through to the synthesized default, so the organization's own fields and the existing record-header actions (Set Active, Edit, Delete, Leave) are preserved". No in-repo page, component or fixture declares an organization edit FORM; the details region is synthesized by the console from the object's fields.
  • update_organization is the only edit-shaped row action: mode: 'edit', locations: ['list_item'], target better-auth organization/update, params name / slug / logo only. The four extension columns are deliberately not better-auth additionalFields (the ai_access / locale notes in auth-manager.ts), so that action cannot carry them.
  • Before this PR the generic edit affordance on this object was bucket-default OFF (managedBy: 'better-auth', no userActions); this PR turns it on, which is the metadata signal the console's record page reads for a generic edit form.
  • No dogfood test exercised a user-context write to sys_organization before this PR (git grep over packages/qa: the only hits insert under SYSTEM_CTX; control: sys_api_key PATCH tests hit). This PR adds the first.

NOT MEASURABLE HERE, and where the answer lives: whether the objectui console's record page renders an edit form for sys_organization from userActions.edit, whether that form honours per-field readonly (so name / slug / logo / metadata render non-editable and the four ruled columns editable), and whether the update_organization param dialog keeps name / slug / logo editable now that those fields are readonly (the sys_user.email precedent under create_user / invite_user has shipped that way since ADR-0092 D4). All three are properties of objectstack-ai/objectui at the pinned .objectui-sha a472b07167a39e55491109e864bb5a54027dcfbd; packages/console/dist is absent from this checkout (regenerated by objectui:refresh), so the bundle could not be grepped either. No objectui card is filed from this lane; the director's execution note reserves that follow-up.

Semver — minor, and the rule applied

  • .github/workflows/pr-automation.yml:667-670 (the Check Changeset step's WHICH LEVEL prose): "A purely additive widening of a published package's public surface (a new exported symbol on an index, a new accepted key or value) takes at least minor. The commit type may raise a bump but never lower it below what the act requires".
  • scripts/check-changeset-no-major.mjs:70-73: "CHOOSING BETWEEN THE TWO LEVELS THIS GUARD LEAVES: a purely additive widening of a published package's public surface takes at least minor, and the commit type may raise a bump but never lower it".
  • scripts/check-changeset-no-major.mjs:7-9 (the LEVEL axis, [finding] No gate answers whether a changeset's LEVEL fits the surface — Check Changeset is green on patch and on minor for the same diff #16055): "a PR that DECLARES clause ② (a new key on a published payload) may not grade a package it grew patch".
  • AGENTS.md:1027 — "Add a changeset for anything that publishes."

The act: a new accepted value (update) in the accept set of a published door on a published package. ⇒ at least minor; major is refused during the launch window and nothing here is breaking (no key, export or accepted value removed; every request that succeeded before succeeds unchanged; the 405 → 403 change touches only requests that were refused and are refused still). node scripts/check-changeset-no-major.mjs --base origin/main ran green in the derived gate set.

Verification (final head f0b8c0d363, exit codes captured after a redirect)

Build. pnpm --filter '@objectstack/plugin-auth^...' --filter '@objectstack/plugin-auth' build then pnpm --filter '@objectstack/dogfood^...' build, both under scripts/pm/os-verify-lock.shVERDICT command-exit 0; rebuilt again after merging origin/main at fc0a783ad5 (4 commits: tests plus hono adapter / cli / client sources and a 3-line lockfile change; pnpm install --frozen-lockfile exit 0). A second merge of origin/main at c8e5ac645f (cli sources and tests, a docs page, scripts/cross-package-test-inputs.mjs) is the final head f0b8c0d363; the gate whose family file moved (check:cross-package-test-inputs) was rerun on that tree, exit 0.

Unit pins (vitest run --maxWorkers=2, under the lock, VERDICT command-exit 0):

  • platform-objects: sys-organization-update-door.test.ts, managed-api-method-affordance-sweep.test.ts, org-hierarchy-timezone.test.tsTest Files 3 passed (3), Tests 17 passed (17).
  • plugin-auth: sys-organization-update-door.test.ts, managed-extension-fields.test.ts, identity-write-guard.test.ts, org-hierarchy-timezone-write-contract.test.tsTest Files 4 passed (4), Tests 99 passed (99).
  • spec: api-methods-batch-conformance.test.ts, managed-api-affordance.test.tsTest Files 2 passed (2), Tests 19 passed (19).

Real door. pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/organization-update-door.dogfood.test.ts — round 1: Tests 7 passed (7); this round, with the four new pins: Test Files 1 passed (1), Tests 11 passed (11), VERDICT command-exit 0.

Patch-round head fc6cfffe42 (edits: changeset body, the ledger row, the dogfood file — no source moved). Typecheck on it, after the last edit: platform-objects exit 0 · plugin-auth exit 0 · spec exit 0 · dogfood exit 0. Pins on it: platform-objects 17 passed (17), plugin-auth 99 passed (99), spec 19 passed (19), dogfood 11 passed (11).

Typecheck on the final head f0b8c0d363, after the last edit: pnpm --filter @objectstack/platform-objects typecheck exit 0 · spec exit 0 · plugin-auth exit 0 · dogfood exit 0. (The first pass on this head reported plugin-auth and dogfood exit 2 with TS7016 on @objectstack/plugin-auth: packages/plugins/plugin-auth/dist/index.d.ts had vanished from this worktree between two runs — present at the green 2740ba273d pass, absent later, dist/index.js still there, OS_SKIP_DTS unset. pnpm exec turbo run build --filter=@objectstack/plugin-auth --force (27 tasks, 0 cached) restored it and both typechecks went exit 0 on the same head; AGENTS.md names this shape for the shared .turbo/cache. Noted below, not filed — not reproduced.) On this head the pin suites were rerun too: platform-objects Tests 10 passed (10), plugin-auth 49 passed (49), spec 4 passed (4), dogfood 7 passed (7). Coverage of the new tests proven with tsc --listFiles: organization-update-door.dogfood.test.ts in dogfood's program (1), sys-organization-update-door.test.ts in plugin-auth's tsconfig.test.json program (1) and in platform-objects' (1); control sys-user.object.ts (1).

Ablation — both legs traced, prediction written first (script header in the scratchpad, quoted here): mutate the ONE line back to apiMethods: ['get', 'list'], leaving userActions and the readonly flags in place. Predicted: platform-objects pin RED on 2 of 6 (the exact-verb pin and the positive control), plugin-auth pin GREEN (the column gate does not read apiMethods — insensitive by design), dogfood RED on 6 of 7 (only "create/delete still 405" green). Resolver: plugin-auth and dogfood import @objectstack/platform-objects through exportsdist/ (both are KNOWN_UNALIASED_TEST_IMPORTS rows), so each leg rebuilt platform-objects and property-read the built artifact before any colour was believed — the preflight script's own prescription where a substring marker cannot be unique (sys_api_key carries the same literal).

  • Mutated leg: on-disk grep -c injected 1 / removed 0; pnpm --filter @objectstack/platform-objects build exit 0; dist/identity/index.js["get","list"], index.mjs["get","list"]. platform-objects Tests 2 failed | 4 passed (6) (the two predicted), plugin-auth Tests 21 passed (21), dogfood Tests 6 failed | 1 passed (7) — the transition pin's first failure: expected { status: 405, … } to not deeply equal { status: 405, … }, the landing pins expected 405 to be 200. Exactly the prediction.
  • Restore leg: git checkout HEAD -- /home/user/objectstack-issue-15873/packages/platform-objects/src/identity/sys-organization.object.ts (absolute path, also the trap … EXIT INT TERM body); git hash-object = 413641ce65b707658051dcb698b491e6fc4cec91 = HEAD: blob (non-empty, equal); git status --porcelain -- path empty; git diff HEAD --stat empty for the whole tree; on-disk injected 0 / restored 1; rebuild exit 0; dist["get","list","update"] on both CJS and ESM; platform-objects 6 passed (6), plugin-auth 21 passed (21), dogfood 7 passed (7).

Gates — derived from git, no hand-listed paths: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 2740ba273d → 78 commands, re-derived at f0b8c0d363 → the identical 78 (the derivation's change set: the six files of this PR). All run (each exit captured after a redirect; per-command timeout 420). 76 exit 0. Two did not measure on the first pass:

  • pnpm check:dual-build-cjs-loads — exit 3, in its own words: "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/." Rerun on f0b8c0d363 after the forced plugin-auth rebuild: exit 3, same words (its 93-case self-test passes first). NOT MEASURED — a package outside this card's built closures has no dist/, and it was not chased with a workspace build.
  • pnpm check:type-check-debt — exit 3, in its own words: "check-type-check-coverage: PREREQUISITE NOT MET — --re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk -- @objectstack/plugin-auth." (The first, runner-timed attempt was recorded exit 124 by my timeout 420; that number is the runner's — its log ends in the same refusal.) That named prerequisite is the vanished plugin-auth/dist/index.d.ts above, restored by the forced rebuild; the real command was then run again on f0b8c0d363 and refused differently, in its own words: "tsc exited null for /tmp/objectstack-debt-remeasure-2nhs51/tsconfig.debt-remeasure.json but printed no recognisable diagnostics -- refusing to record 0", with V8 "allocation failure" traces at about 4030 MB under --max-old-space-size=4096 — heap-bound on this box, not chased per the seat's note. NOT MEASURED.
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions, which the seat expected to refuse for the same reason, MEASURED green here (exit 0; its five self-test verdict lines print).

Reconciliation, verbatim: Run reconciliation — 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN. and ✓ dispatch-gates --ran: 78 derived famil(ies) accounted for — 78 run, 0 NOT-MEASURED. The derivation printed a STALE TREE notice (origin/main moved again after my merge; one file it derives from changed across that range) — recorded in the report for the PM's half.

Gates, patch-round head fc6cfffe42. Re-derived: the identical 78 commands (change set: the same six files). Rerun on this tree, lock-free, exit after a redirect: the 32 node scripts/… gates 32/32 exit 0; the 18 spec check:* 18/18 exit 0 — four of them (check:api-surface, check:dual-source-exports, check:entry-nameability, check:exported-any) first refused with "packages/spec/dist/**/*.d.ts describe DIFFERENT sources than the ones on disk … packages/spec/src no longer hashes to it", because the ledger row lives in a test file under packages/spec/src and was edited after this worktree's closure build; pnpm --filter @objectstack/spec build (VERDICT command-exit 0, check-dts-emitted: 34/34) re-stamped it and all four went exit 0 (public API surface + factory signatures unchanged ✓); the 26 pnpm check:* families minus the two refusals 26/26 exit 0. check:dual-build-cjs-loads rerun in this worktree: exit 3, same words ("PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/."). check:type-check-debt: carried from f0b8c0d363 in its own words (heap-bound refusal), not rerun — its inputs (the ledgered packages' built closure, the 4 GB ceiling) did not move with a changeset and two test files. Reconciliation on fc6cfffe42, verbatim: Run reconciliation — 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN. and ✓ dispatch-gates --ran: 78 derived famil(ies) accounted for — 78 run, 0 NOT-MEASURED.

Also run: check:nul-bytes (in the derived set, exit 0) and the control-character self-scan over every touched file (0 hits; control printf of a NUL into a scratch file: 1 hit).

验收备注 (acceptance notes — observations, none filed)

  • noted, not filed: the plain single-tenant showcase boot holds ZERO sys_organization rows (system-context find[]) and the seeded admin's session carries activeOrganizationId: null, while setup-nav.contributions.ts says "plugin-auth's single-org default-org bootstrap guarantees an org to invite into" and the {current_org_id} nav entry "falls back to the sys_organization list — one row in single-org". In this harness there is no row for that fallback to show; the door test therefore boots multiTenant: 'posture-only' and mints its org through better-auth. Whether a real single-org deployment mints the default-org row lazily (first invite) or at bootstrap was not measured here.
  • noted, not filed: SysOrganizationDetailPage's comment names "Edit" among the preserved record-header actions, but the only edit-shaped row action on the object is update_organization with locations: ['list_item']; which "Edit" the console renders on the record header (and from which declaration) is an objectui reading.
  • noted, not filed: check:type-check-debt took more than 420 s on this shared box to reach its refusal (my runner's timeout recorded 124 while the log ends in the gate's own PREREQUISITE NOT MET) — a foreground run of that gate here is a cap risk, not a red.

Contract review

Clause-② carriers: needs:contract-review on the card (already) and on this PR (applied at opening, read back). node scripts/pm/check-clause2-carriers.mjs --pair exit code is in the report comment on #15873.

… column-gated by the ADR-0092 D2 whitelist (#15873)

Ruling (a), decision batch #64, 2026-09-07 — the data door admits `update`
and the identity write guard's per-object whitelist does the column gating.
`enable.apiMethods` becomes ['get', 'list', 'update'] with the
`userActions.edit` affordance the registry reconciler requires; better-auth's
own columns (name / slug / logo / metadata) are readonly per ADR-0092 D4 and
stay refused/stripped by the guard. create / delete still 405; no bulk
(recorded in SINGLE_RECORD_WRITE_ONLY).

Pins: declaration (platform-objects), column gate + D4 partition derived from
the shipped whitelist (plugin-auth), and the real door with the 405 → 403
transition (dogfood).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…under the walled posture

The plain showcase boot holds no sys_organization row (system-context read
returns []; the admin session has activeOrganizationId null), and
organization/create is denied without an organization wall (#5261) — so the
fixture boots multiTenant: 'posture-only' and creates the org the way the
Setup app does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
@github-actions github-actions Bot added the size/l label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/platform-objects, touching 2 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/objects.mdx (via userActions (symbol, a field of const object SysOrganization))
  • content/docs/protocol/objectui/index.mdx (via userActions (symbol, a field of const object SysOrganization))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via userActions (symbol, a field of const object SysOrganization))
  • content/docs/releases/v16.mdx (via userActions (symbol, a field of const object SysOrganization))
  • content/docs/releases/v17.mdx (via userActions (symbol, a field of const object SysOrganization))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 001a83b0486391847d45f6866896c53ad8714569packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4f859d08d0c27f5b8520fbb46dfe19e59530fecb — the merge of head fc6cfffe42e81ee02da16c136c853c316f65743d into base 001a83b0486391847d45f6866896c53ad8714569, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4f859d08d0c27f5b8520fbb46dfe19e59530fecb && git checkout 4f859d08d0c27f5b8520fbb46dfe19e59530fecb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 001a83b0486391847d45f6866896c53ad8714569 fc6cfffe42e81ee02da16c136c853c316f65743d && git checkout -B drift-repro 001a83b0486391847d45f6866896c53ad8714569 && git merge --no-ff fc6cfffe42e81ee02da16c136c853c316f65743d

node scripts/docs-audit/affected-docs.mjs --json 001a83b0486391847d45f6866896c53ad8714569

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 001a83b0486391847d45f6866896c53ad8714569 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…e grant moves (#15873 contract-review patch round)

Review FAIL was on the accept-set statement, not the code: granting `update`
also derives the update-mode import door (API_METHOD_DERIVATION: import =
any of create/update) and flips /auth/me/permissions for sys_organization
(allowEdit true, apiOperations gains update and import). Both named in the
changeset; pinned on the real door in the dogfood file (import: 200, timezone
lands, name stripped; better-auth-only row refused per row; insert mode 405
naming create; /me/permissions allowEdit true with update+import and a
sibling better-auth table as the clamp control); the SINGLE_RECORD_WRITE_ONLY
sentence now says what is true (bulk not granted; the derived import door
is, column-clamped per row).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
@os-musk
os-musk marked this pull request as ready for review September 7, 2026 22:02
@os-musk
os-musk enabled auto-merge September 7, 2026 22:02
@os-musk
os-musk added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 7797102 Sep 7, 2026
42 checks passed
@os-musk
os-musk deleted the claude/issue-15873-organization-update-door branch September 7, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

2 participants