Repository navigation
feat(rest, client): a tenant-admin REST door for the record-currency code backfill - #30809
Conversation
…code backfill POST /meta/_migrate-record-currency-codes runs backfillRecordCurrencyCodes for the caller's current organization only, gated on setup.write and mounted behind metadata.endpoints.maintenance; preview unless apply is true. Adds the SDK method, the REST route-ledger row, the objectql barrel re-export of the two organization report types, the 409 remedy naming the door, and a truncated walk that no longer claims every row was examined. Claude-Session: https://claude.ai/code/session_016WV4cyrhYJgbkZHbbqud3F Co-authored-by: Claude <noreply@anthropic.com>
…the truncated walk text Claude-Session: https://claude.ai/code/session_016WV4cyrhYJgbkZHbbqud3F Co-authored-by: Claude <noreply@anthropic.com>
… a refused row reads the hook's sentence The new `POST /meta/_migrate-record-currency-codes` write door joins the censuses that enumerate every REST write door and registrar module: - the `/meta` write-door enumeration (a DATA door, gated on `setup.write`, with its own refusal and control cases), the #8497 record-echo tripwire (no record echo), the #22694 hook-refusal ledger (driven per row), the registrar file lists, the objectQLProvider consumer census (7 sites), the query-slot census (no slot) and the docblock floors. Driving it through the hook-refusal ledger found a real leak: a row whose code write a sandboxed hook refused reported the `hook '<name>' threw:` debug wrapper as its failure text, and a crashed hook's row its native error. The backfill now reads the throw as the import row does: the hook's own sentence for a refusal, the `/data` door's withheld sentence for a 5xx. The operator's log line keeps the whole text. Measured by ablation: without the objectql change, 12 rows of the ledger go red. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyrhYJgbkZHbbqud3F
…cy backfill door `check:system-context-census` reds on an elevation read no row owns: the new door's capability gate lets `isSystem` through. Row 50b names it, and the generated register is rewritten with `--write`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyrhYJgbkZHbbqud3F
…cy backfill door `metadata.endpoints.maintenance` now also mounts `POST /meta/_migrate-record-currency-codes`, and each endpoints switch's describe() enumerates every mount it gates. The reference page is regenerated from the schema. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyrhYJgbkZHbbqud3F
📓 Docs Drift CheckThis PR changes 6 package(s): 38 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 182 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 5f552fc5951bf2322833ea08f9b87d053ee2e045 && git checkout 5f552fc5951bf2322833ea08f9b87d053ee2e045
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7da99b79f5fcf3fa1dfc143f39d73fb702dfb271 12af728fa8a19542811c866268a2bbb27cd105c4 && git checkout -B drift-repro 7da99b79f5fcf3fa1dfc143f39d73fb702dfb271 && git merge --no-ff 12af728fa8a19542811c866268a2bbb27cd105c4
node scripts/docs-audit/affected-docs.mjs --json 7da99b79f5fcf3fa1dfc143f39d73fb702dfb271
|
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW Contract reviewServed-tier: Read: card #30675 (body, all 7 comments: claim 6675312689, amendments 6675340866 and 6675450053, dev report 6677009085), ruling 6670707546 item B, A card #30674 and its ACCEPT 6673657336, domain:spec#2's consent 6675431663, the PR body, and ① Derived judgmentsShape.
Observed, not blocking.
② Semver level
The spec hunk against consent 6675431663, line by line. One file under File surface against the claim and its amendments. Inside: Check-runs on the head at 2026-11-06T04:51Z: 46 runs: 25 completed (15 success, 9 skipped, 1 failure), 21 in progress, 0 queued. Of the ten required contexts, ③ Boundary flags
Implemented-by: VERDICT: PASS Must-fix: none. Should-fix (for the ACCEPT or a follow-up card, none blocking this head):
Generated by Claude Code |
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW 席位核对 · ACCEPT · 2026-11-06T05:29Z · #30675Head-sha: 12af728
|
Requested by Elon · project thread
domain:server#1 · os-elon-musk · session_016WV4cyrhYJgbkZHbbqud3F
Fixes #30675
Clause-②: yes (widening) — a new route
POST /api/v1/meta/_migrate-record-currency-codes(behindmetadata.endpoints.maintenance), a new SDK methodclient.meta.migrateRecordCurrencyCodes, and two type exports on the@objectstack/objectqlentry (RecordCurrencyBackfillOrganization,RecordCurrencyBackfillOrganizationsRefusal).Metadata-protocol: none
Claim: 6675312689 (amended 6675340866, 6675450053) · Branch:
claude/project-thread-1lyw0v· Ruling-ref: 6670707546 (letter C, item B)维护者速读
POST /api/v1/meta/_migrate-record-currency-codes,请求体{ object, field, code, apply? },只补调用者当前组织的无码行;不带apply: true是预览、不写;SDK 加client.meta.migrateRecordCurrencyCodes。409destructive_change的补救文字现在同时点名 CLI 和这个门。currencyMode: 'record'后,老行有金额没币种,补救只有os migrate record-currency-codes,云上租户管理员碰不到数据库 shell(裁决 6670707546 C 的 B 项)。setup.write,请求体不收任何组织参数(organizationId等一律 400),无当前组织 403,maintenance开关关掉就不挂载。顺带修了一处泄漏:被沙箱钩子拒写的行,报告里原来是hook '<name>' threw:调试包装,现在是钩子自己的那句话。回滚即撤 PR。packages/rest/src/meta-record-currency-backfill-route.ts(门禁顺序、组织取自ctx.tenantId、请求体白名单),packages/objectql/src/record-currency-backfill.ts的rowFailureSentence,packages/rest/src/meta-write-door-capability-enumeration.test.ts新增的DATA_DOORS一节。pnpm --filter @objectstack/rest exec vitest run src/meta-record-currency-backfill-route.test.ts,预期 12 条全绿,其中「the other organization's admin sees and writes only its own row」断言 B 组织管理员只碰到b1、A 组织的行一行未动。What changes
@objectstack/rest: the door (meta-record-currency-backfill-route.ts, new; registered fromregisterMetadataEndpointsInnerbeside the object state read)POST {metaPath}/_migrate-record-currency-codes, on every mount base the/metafamily is mounted on, behindmetadata.endpoints.maintenance(the_migrate-storedswitch). It sits inside the/metaumbrella, so an anonymous caller gets the umbrella's 401.setup.writeinsystemPermissions(resolved for the current organization), orisSystem. Otherwise403 FORBIDDEN. This is asked first, before the body or the engine, so a refused caller learns nothing about objects or kernels.ctx.tenantId. None means403 FORBIDDEN("no current organization … Nothing was read.").{ object, field, code, apply? }. Any other key is400 VALIDATION_ERRORnaming it.organizationIdandallOrganizationsget their own sentence ("this door fills the rows of your current organization only"). Blank names and a non-booleanapplyare refused the same way.wiredEngineOrLoud.@objectstack/objectqlstays a devDependency and is loaded by a guarded dynamic import. No engine, or no function, means501 NOT_IMPLEMENTED.backfillRecordCurrencyCodes(ql, logger, { object, field, code, apply, organizationId }). The answer is{ success: true, data: <report> }, the report the CLI's--jsonprints.INVALID_FIELDwithdetails.fields,OBJECT_NOT_FOUND404); anything else goes tosendServerFault.REST_ROUTE_LEDGER(disposition: 'sdk', noauthzfield, because there is no conformance coverage yet),REST_SERVER_REGISTRAR_FILESandREST_SERVER_META_ROUTE_FILES, and thescripts/check-undeclared-dep-imports.mjsLEDGER (oneoptional-runtime-proberow, seat amendment 6675450053).@objectstack/client:client.meta.migrateRecordCurrencyCodes({ object, field, code, apply? })returnsunknown(see Acceptance notes).@objectstack/metadata-protocol: thecurrency_mode_to_recordfinding of the 409destructive_changenow names the route and its body beside the CLI command.@objectstack/objectqlRecordCurrencyBackfillOrganizationandRecordCurrencyBackfillOrganizationsRefusal(carried item 1 from the cli + objectql: record-currency backfill scoped per organization —--org, refuse multi-org runs without it,--all-organizationsexplicit; recompute takes the same--org(multi-currency, epic #8345, ruling C item A) #30674 ACCEPT).truncated. The text no longer prints "✓ Every … nothing to backfill" for rows it never examined. An unscoped run adds that its organization check saw only the rows examined. The CLI prints aWalk incompletewarning. REST and CLI answer the same report.errorreads a sandboxed hook as the write doors do (rowFailureSentence, the import row's rule). A refusal gives the hook's own sentence. A crash, a declared 5xx or the sandbox's own fault gives the/datadoor's withheld sentence. Any other throw keeps its message. The operator'slogger.warnline keeps the whole text.Docs
content/docs/deployment/cli.mdx: a truncation paragraph, then "Without shell access, use the route" with the HTTP and SDK forms. It covers the caller's organization only, the refusals, the capability and the switch, and says there is no Setup button yet.content/docs/data-modeling/currency.mdx: one sentence.content/docs/permissions/system-context.mdx: row 50b and the regenerated register.Tests
packages/rest/src/meta-record-currency-backfill-route.test.ts(new, 12 cases, realObjectQLover an in-memory driver, two organizations):b1.isSystemwith a current organization passes the gate and stays in that organization.setup.writegives 403 with the provider never called; no current organization gives 403;organizationIdorallOrganizationsin the body gives 400; a malformed body gives 400; a non-record-mode field givesINVALID_FIELDwithdetails.fields; an unknown object gives 404; no engine gives 501.maintenanceswitch off unmounts the route and the other switches leave it; the scoped base mounts it.meta-write-door-capability-enumeration.test.ts: the door joins the closed set of/metawrite doors as aDATA_DOORSentry. It is not amanage_metadatadoor: it reaches no protocol method. It has its own cases. Anonymous gets 401. A capability-less caller, amanage_metadataholder andsetup.accessalone each get 403.setup.writeandisSystempass the gate on both bases, reaching 501 with no engine.rest-write-route-hook-refusal-sentence.ledger.test.ts: the door is driven per row (§2 and §6, 12 shapes), with the failure entry as the answer. The harness caller gainedsetup.writeand an organization, and its engine double gainedgetObject. The whole file is green.no-record-echo), theobjectQLProviderconsumer census (6 → 7 sites, all throughwiredEngineOrLoud), the query-slot census (0 slots), the docblock floors (7 blocks), andrest-config-mount-table.pin.test.ts(the maintenance row gates four routes).packages/objectql/src/record-currency-backfill.test.ts: a walk cut short bymaxRecordssays so, and the new describe "a failed row reads a sandboxed hook as the write doors do" (a refusal, a crash and an own fault, and a plain-throw control).packages/cli/src/commands/migrate/record-currency-codes.test.ts:--org org_a --max-records 2prints the incomplete-walk warning.packages/client: the method POSTs the body and unwrapsdata, andapplyis sent only when true; its return type is pinned asunknown.protocol-destructive-currency-mode.test.ts: the remedy names the route.Acceptance notes
domain:specconsent.packages/spec/src/api/rest-server.zod.ts(L538–541) said the switch mountsGET /meta/diagnostics,GET /meta/_draftsandPOST /meta/_migrate-stored, and did not name this door. Commit12af728faddsPOST /meta/_migrate-record-currency-codesin the sentence's own list shape and nothing else underpackages/spec/src. It also carries the regeneratedcontent/docs/references/api/rest-server.mdxrow and'@objectstack/spec': patchin the card changeset, underdomain:spec#2's consent 6675431663 (mailbox PR pm:mailbox — domain:spec#2 · os-sales #30557). The hunk raises this PR's review to CONTRACT_REVIEW_TIER.unknown, not the report type. Ruling 2C and the seat's amendment keep it untyped likemigrateStored. Ananyreturn would red the shrink-onlypackages/client/exported-any-returns.json, so it isunknown. Callers who want the type import it from@objectstack/objectql._migrate-storedanswers bare. The new module is born onsendOk/sendError/sendServerFault(check:route-envelopeasks new modules for the shared pair). So the report isdata, and refusals are{ success: false, error: { code, message } }.{ isSystem: true, tenantId: <current organization> }as the CLI's do, somodified_byis not the admin's id. The door's gate is the attribution point.organization_id) is refusedINVALID_FIELD/ 400 by the backfill. Its sentence says to run it without an organization, which is the CLI's form; over REST there is no such form, by design.Co-authored-by: ClaudeplusClaude-Session.Verification
Local tests and type-checks (head b56a12d)
pnpm --filter @objectstack/rest exec vitest run(whole package): 396 files passed; 7710 tests passed, 354 skipped.pnpm --filter @objectstack/objectql exec vitest run(whole package): 557 files, 11089 tests passed.pnpm --filter @objectstack/client exec vitest run: 54 files, 703 tests passed.pnpm --filter @objectstack/metadata-protocol exec vitest run: 319 files passed (4 skipped); 22480 tests passed.pnpm --filter @objectstack/cli exec vitest run --project integration src/commands/migrate/: 49 files passed (1 skipped); 375 tests passed. The migrate tests live inintegration.pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts(they read the registrar lists): 70 passed.tsc --noEmitfor rest, client, metadata-protocol and objectql: exit 0.turbo run buildunder the verify lock: 119/119 tasks, VERDICT command-exit 0.pnpm lint: exit 0.Ablation
The failure-sentence fix was measured as follows.
rowFailureSentence(err, object, message)was replaced bymessageinrecord-currency-backfill.ts, and@objectstack/objectqlwas rebuilt, because rest resolves it throughdist.rest-write-route-hook-refusal-sentence.ledger.test.tsthen gave 12 failed and 613 passed. After restoring the file and rebuilding, it gave 625 passed.Derived gates (dispatch-gates --commands, reconciled with --ran)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 116 families on headb56a12d6. Each was run with its exit code captured before any pipe.The first pass, at an earlier head, had one finding:
check:system-context-censusexited 1, because the door'sisSystemcapability bypass was a read no row owned. Row 50b now owns it, and the generated register was rewritten with--write.The second pass had one prerequisite miss:
pnpm --filter @objectstack/spec run check:docsexited 1 with "packages/spec/json-schema is older than packages/spec/src". That is a gitignored local artifact, made stale by preparing the held spec hunk. Afterpnpm --filter @objectstack/spec gen:schema, it exited 0 ("241 generated files in sync"). The CI families outside the derivation are read from the PR's check-runs, not rerun here.Human Minimum
packages/specholds no runtime logic. (packages/specgains one describe string only, at12af728f.)pnpm lintpasses.main, one pull request.packages/spectouched: one describe hunk, underdomain:specconsent 6675431663; the generated docs row comes fromgen:docs, andcheck:generatedpasses all 14 artifacts.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code