feat(platform-objects): sys_organization's data door admits update, column-gated by the ADR-0092 D2 whitelist - #16687
Conversation
… 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
…ganization-update-door
…ganization-update-door
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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
Fixes #15873
Clause-②: yes.
enable.apiMethodsis the accept set of a published door —packages/rest/src/rest-server.ts(apiAccessDecision, called byenforceApiAccess) refuses an unlisted operation with 405OBJECT_API_METHOD_NOT_ALLOWEDbefore the engine is reached — and the value ships inpackages/platform-objects/dist/identity/index.js/index.mjs. This PR changes whatPATCH /api/v1/data/sys_organization/:idaccepts on a published identity object, so it carriesneeds: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: 「同意」):
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:1699gate:enable.apiMethods: ['get', 'list']→['get', 'list', 'update'].create/deletestill answer 405;bulkis not granted.updatealso derives the update-modeimportdoor, and the affordance flips/auth/me/permissions.userActions: { edit: true }— required, not optional: amanagedByobject runs throughreconcileManagedApiMethods(objectql registry, ADR-0092 / ADR-0103 D3), which strips any write verb the resolved affordances do not grant and only warns. Without this lineupdatewould 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 onsys_api_key. The repo-wide sweepmanaged-api-method-affordance-sweep.test.tsand the object's own pin both hold it.name,slug,logo,metadata→readonly: true. ADR-0092 D4's form-rendering constraint ("non-whitelisted fields must render non-editable in the standard edit form") — the same treatmentsys_user(email,role, …) andsys_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 stampscontext.isSystem: true, so better-auth'sorganization/updatestill writes these columns (thesys_user.emailprecedent).packages/spec/src/data/api-methods-batch-conformance.test.ts—sys_organizationadded toSINGLE_RECORD_WRITE_ONLYwith its reason. The ledger forces a choice between grantingbulkand recording why not; the ruling widened one verb, andbulk(the/batchand*Manyroutes on an identity table) is a further widening it did not take. The stale-entry check refusesbulkbeing added later without retiring the row..changeset/organization-update-door.md—@objectstack/platform-objects: minor, stating the widening and the held column gate.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.
PATCH /api/v1/data/sys_organization/:idis admitted (apiMethodsgainsupdate,userActions.editkeeps it through registration).create/deletestay 405;bulk(/batch,*Many) is not granted.importdoor.packages/spec/src/data/api-derivation.tsAPI_METHOD_DERIVATION:import: { any: ['create', 'update'] }— grantingupdatederivesimport, soPOST /api/v1/data/sys_organization/import(and the async/import/jobsroute) is admitted inwriteMode: '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 iscreate. The door's own 405 envelope already advertised it inallowed./auth/me/permissions.packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:clampManagedObjectWritesreadsuserActions.editfor thebetter-authbucket,annotateEffectiveApiOperationsreports the effective set — for a principal the permission layer already admits,sys_organization.allowEditgoesfalse → trueandapiOperationsgainsupdateandimport. This is the payload the console renders its edit affordance from.organization_admin/member_defaultstay hard-denied on every better-auth table bymanaged-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_ONLYrow now says what is true:bulkis not granted; the derived update-mode import door is, column-clamped per row — the ledger is aboutbulk(/batch,*Many), whichimportdoes not read, so the exemption stands beside it.Pins for 2 and 3, on the real door (
organization-update-door.dogfood.test.ts, measured):GET /auth/me/permissions→objects.sys_organizationallowEdit: true,allowCreate: false,allowDelete: false;apiOperationscontainsupdateandimport, notcreate/delete/bulk. Control:objects.sys_member(better-auth, nouserActions) staysallowEdit: falsefor the same principal — the clamp is live, not a wildcard foldPOST /data/sys_organization/import{ writeMode: 'update', matchFields: ['id'], rows: [{ id, name, timezone: 'Asia/Tokyo' }] }updated: 1,errors: 0; read-backtimezone === 'Asia/Tokyo',nameunchangedtreatAsHistorical: true, row carryingnameonlyupdated: 0,results[0].ok === false,results[0].code === 'PERMISSION_DENIED';nameunchanged — no elevationwriteMode: 'insert',rows: [{ name, slug }]OBJECT_API_METHOD_NOT_ALLOWED,errornames'create';allowedcontainsupdateandimport, notcreateNon-vacuity, measured (second ablation, this round; prediction written first in the script header). Cut: add
name(plus an inert unique marker) toMANAGED_EXTENSION_EDITABLE_FIELDS.sys_organization— widen the column gate itself — rebuild plugin-auth (dogfood consumes it throughdist/),ablation-dist-preflight.mjs @objectstack/plugin-auth ablation_15873_marker✓ present indist/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, becausenameisreadonly(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).
reconcileManagedApiMethodsruns at registration, not at build, so a property-read ofdist/cannot see anything it touches — withuserActionsremoved,diststill reads["get","list","update"]while the registered schema reads["get","list"]. Which instrument each claim here rests on:apiMethods/userActions/readonlyship in the artifact —dist/property-read;updatesurvives 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/permissionsresponse, 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.tsboots the showcase stack, signs in as the seeded platform admin, mints an organization through better-auth'sorganization/create(thecreate_organizationrow action's target) and drivesPATCH /api/v1/data/sys_organization/:idunder that user context:{ name }(better-auth column, alone)OBJECT_API_METHOD_NOT_ALLOWEDPERMISSION_DENIED— the guard's own verdict;nameunchanged on read-back{ timezone: 'Asia/Shanghai', sort_order: 7 }{ require_mfa }(alone, at its current value){ sort_order: 9, name, slug, logo, metadata }(smuggled)sort_orderlands, the four better-auth columns read back unchanged; the guard logsstripped non-whitelisted field(s) … name, slug, logo, metadata (ADR-0092){ timezone: 'Mars/Olympus' }VALIDATION_FAILED— #14238'svalueDomain: 'iana_time_zone'now judges the doorPOST /data/sys_organization,DELETE /data/sys_organization/:idOBJECT_API_METHOD_NOT_ALLOWED(unchanged)Plus the REGISTERED schema (post-reconcile) contains
update, notcreate/delete/bulk, withuserActions.edit === true; andupdate_organizationstill targets/api/v1/auth/organization/updatewithname/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.tsdrives the guard'sbeforeUpdatewith the SHIPPED whitelist (registered frommanagedExtensionEditableFields('sys_organization'), not re-spelled): each ofname/slug/logo/metadataalone → thrown error withcode: '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 orreadonly, never both, never neither.require_mfais written at its current value on the door rather than flipped:require_mfa: trueon 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 atwarn, and the whitelisted column lands. That is ADR-0092 D2 as accepted ("Non-whitelisted keys are stripped; if the payload becomes empty … throws"), identical tosys_userandsys_api_key, and the reason D4'sreadonlyflags 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.ts—nav_organizationmounts the ACTIVE organization's record page (objectName: 'sys_organization',recordId: '{current_org_id}') in the Setup app for every deployment, andnav_organizationsthe list, gated onorg-scoping.packages/platform-objects/src/pages/sys-organization.page.ts—SysOrganizationDetailPage, 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_organizationis the only edit-shaped row action:mode: 'edit',locations: ['list_item'], target better-authorganization/update, paramsname/slug/logoonly. The four extension columns are deliberately not better-authadditionalFields(theai_access/localenotes inauth-manager.ts), so that action cannot carry them.managedBy: 'better-auth', nouserActions); this PR turns it on, which is the metadata signal the console's record page reads for a generic edit form.sys_organizationbefore this PR (git grepoverpackages/qa: the only hits insert underSYSTEM_CTX; control:sys_api_keyPATCH 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_organizationfromuserActions.edit, whether that form honours per-fieldreadonly(soname/slug/logo/metadatarender non-editable and the four ruled columns editable), and whether theupdate_organizationparam dialog keepsname/slug/logoeditable now that those fields arereadonly(thesys_user.emailprecedent undercreate_user/invite_userhas shipped that way since ADR-0092 D4). All three are properties ofobjectstack-ai/objectuiat the pinned.objectui-shaa472b07167a39e55491109e864bb5a54027dcfbd;packages/console/distis absent from this checkout (regenerated byobjectui: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(theCheck Changesetstep's WHICH LEVEL prose): "A purely additive widening of a published package's public surface (a new exported symbol on anindex, a new accepted key or value) takes at leastminor. 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 leastminor, 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 grewpatch".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 leastminor;majoris 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/mainran 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' buildthenpnpm --filter '@objectstack/dogfood^...' build, both underscripts/pm/os-verify-lock.sh—VERDICT command-exit 0; rebuilt again after mergingorigin/mainatfc0a783ad5(4 commits: tests plus hono adapter / cli / client sources and a 3-line lockfile change;pnpm install --frozen-lockfileexit 0). A second merge oforigin/mainatc8e5ac645f(cli sources and tests, a docs page,scripts/cross-package-test-inputs.mjs) is the final headf0b8c0d363; 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):sys-organization-update-door.test.ts,managed-api-method-affordance-sweep.test.ts,org-hierarchy-timezone.test.ts—Test Files 3 passed (3),Tests 17 passed (17).sys-organization-update-door.test.ts,managed-extension-fields.test.ts,identity-write-guard.test.ts,org-hierarchy-timezone-write-contract.test.ts—Test Files 4 passed (4),Tests 99 passed (99).api-methods-batch-conformance.test.ts,managed-api-affordance.test.ts—Test 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-objects17 passed (17), plugin-auth99 passed (99), spec19 passed (19), dogfood11 passed (11).Typecheck on the final head
f0b8c0d363, after the last edit:pnpm --filter @objectstack/platform-objects typecheckexit 0 ·specexit 0 ·plugin-authexit 0 ·dogfoodexit 0. (The first pass on this head reportedplugin-authanddogfoodexit 2 with TS7016 on@objectstack/plugin-auth:packages/plugins/plugin-auth/dist/index.d.tshad vanished from this worktree between two runs — present at the green2740ba273dpass, absent later,dist/index.jsstill there,OS_SKIP_DTSunset.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-objectsTests 10 passed (10), plugin-auth49 passed (49), spec4 passed (4), dogfood7 passed (7). Coverage of the new tests proven withtsc --listFiles:organization-update-door.dogfood.test.tsin dogfood's program (1),sys-organization-update-door.test.tsin plugin-auth'stsconfig.test.jsonprogram (1) and in platform-objects' (1); controlsys-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'], leavinguserActionsand 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 readapiMethods— insensitive by design), dogfood RED on 6 of 7 (only "create/delete still 405" green). Resolver: plugin-auth and dogfood import@objectstack/platform-objectsthroughexports→dist/(both areKNOWN_UNALIASED_TEST_IMPORTSrows), 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_keycarries the same literal).grep -cinjected 1 / removed 0;pnpm --filter @objectstack/platform-objects buildexit 0;dist/identity/index.js→["get","list"],index.mjs→["get","list"]. platform-objectsTests 2 failed | 4 passed (6)(the two predicted), plugin-authTests 21 passed (21), dogfoodTests 6 failed | 1 passed (7)— the transition pin's first failure:expected { status: 405, … } to not deeply equal { status: 405, … }, the landing pinsexpected 405 to be 200. Exactly the prediction.git checkout HEAD -- /home/user/objectstack-issue-15873/packages/platform-objects/src/identity/sys-organization.object.ts(absolute path, also thetrap … EXIT INT TERMbody);git hash-object=413641ce65b707658051dcb698b491e6fc4cec91=HEAD:blob (non-empty, equal);git status --porcelain -- pathempty;git diff HEAD --statempty for the whole tree; on-disk injected 0 / restored 1; rebuild exit 0;dist→["get","list","update"]on both CJS and ESM; platform-objects6 passed (6), plugin-auth21 passed (21), dogfood7 passed (7).Gates — derived from git, no hand-listed paths:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat2740ba273d→ 78 commands, re-derived atf0b8c0d363→ the identical 78 (the derivation's change set: the six files of this PR). All run (each exit captured after a redirect; per-commandtimeout 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 onf0b8c0d363after 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 nodist/, 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 mytimeout 420; that number is the runner's — its log ends in the same refusal.) That named prerequisite is the vanishedplugin-auth/dist/index.d.tsabove, restored by the forced rebuild; the real command was then run again onf0b8c0d363and 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 32node scripts/…gates 32/32 exit 0; the 18 speccheck:*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 underpackages/spec/srcand 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 26pnpm check:*families minus the two refusals 26/26 exit 0.check:dual-build-cjs-loadsrerun 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 fromf0b8c0d363in 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 onfc6cfffe42, 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; controlprintfof a NUL into a scratch file: 1 hit).验收备注 (acceptance notes — observations, none filed)
sys_organizationrows (system-contextfind→[]) and the seeded admin's session carriesactiveOrganizationId: null, whilesetup-nav.contributions.tssays "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 bootsmultiTenant: '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.SysOrganizationDetailPage's comment names "Edit" among the preserved record-header actions, but the only edit-shaped row action on the object isupdate_organizationwithlocations: ['list_item']; which "Edit" the console renders on the record header (and from which declaration) is an objectui reading.check:type-check-debttook more than 420 s on this shared box to reach its refusal (my runner'stimeoutrecorded 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-reviewon the card (already) and on this PR (applied at opening, read back).node scripts/pm/check-clause2-carriers.mjs --pairexit code is in the report comment on #15873.