Skip to content

feat(rest, client): a tenant-admin REST door for the record-currency code backfill - #30809

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-1lyw0v
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-1lyw0v

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

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 (behind metadata.endpoints.maintenance), a new SDK method client.meta.migrateRecordCurrencyCodes, and two type exports on the @objectstack/objectql entry (RecordCurrencyBackfillOrganization, RecordCurrencyBackfillOrganizationsRefusal).
Metadata-protocol: none

Claim: 6675312689 (amended 6675340866, 6675450053) · Branch: claude/project-thread-1lyw0v · Ruling-ref: 6670707546 (letter C, item B)

维护者速读

  • 改了什么:组织管理员没有 shell 也能补币种码了。新增 POST /api/v1/meta/_migrate-record-currency-codes,请求体 { object, field, code, apply? },只补调用者当前组织的无码行;不带 apply: true 是预览、不写;SDK 加 client.meta.migrateRecordCurrencyCodes。409 destructive_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 from registerMetadataEndpointsInner beside the object state read)

  • POST {metaPath}/_migrate-record-currency-codes, on every mount base the /meta family is mounted on, behind metadata.endpoints.maintenance (the _migrate-stored switch). It sits inside the /meta umbrella, so an anonymous caller gets the umbrella's 401.
  • The order inside the handler:
    1. The gate: setup.write in systemPermissions (resolved for the current organization), or isSystem. Otherwise 403 FORBIDDEN. This is asked first, before the body or the engine, so a refused caller learns nothing about objects or kernels.
    2. The organization: ctx.tenantId. None means 403 FORBIDDEN ("no current organization … Nothing was read.").
    3. The body: the closed set { object, field, code, apply? }. Any other key is 400 VALIDATION_ERROR naming it. organizationId and allOrganizations get their own sentence ("this door fills the rows of your current organization only"). Blank names and a non-boolean apply are refused the same way.
    4. The engine: the request's environment's, through wiredEngineOrLoud. @objectstack/objectql stays a devDependency and is loaded by a guarded dynamic import. No engine, or no function, means 501 NOT_IMPLEMENTED.
    5. backfillRecordCurrencyCodes(ql, logger, { object, field, code, apply, organizationId }). The answer is { success: true, data: <report> }, the report the CLI's --json prints.
  • Errors use only codes already in the registry. A 4xx the backfill declares is answered with its own status and code (INVALID_FIELD with details.fields, OBJECT_NOT_FOUND 404); anything else goes to sendServerFault.
  • Registered in: REST_ROUTE_LEDGER (disposition: 'sdk', no authz field, because there is no conformance coverage yet), REST_SERVER_REGISTRAR_FILES and REST_SERVER_META_ROUTE_FILES, and the scripts/check-undeclared-dep-imports.mjs LEDGER (one optional-runtime-probe row, seat amendment 6675450053).

@objectstack/client: client.meta.migrateRecordCurrencyCodes({ object, field, code, apply? }) returns unknown (see Acceptance notes).

@objectstack/metadata-protocol: the currency_mode_to_record finding of the 409 destructive_change now names the route and its body beside the CLI command.

@objectstack/objectql

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, real ObjectQL over an in-memory driver, two organizations):
    • The org A admin's preview counts only A's rows and writes nothing; every walk read carries A's filter.
    • The org B admin sees and writes only b1.
    • Apply writes only the caller's codeless rows, never a coded row or another organization's; isSystem with a current organization passes the gate and stays in that organization.
    • The refusals: missing setup.write gives 403 with the provider never called; no current organization gives 403; organizationId or allOrganizations in the body gives 400; a malformed body gives 400; a non-record-mode field gives INVALID_FIELD with details.fields; an unknown object gives 404; no engine gives 501.
    • The maintenance switch 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 /meta write doors as a DATA_DOORS entry. It is not a manage_metadata door: it reaches no protocol method. It has its own cases. Anonymous gets 401. A capability-less caller, a manage_metadata holder and setup.access alone each get 403. setup.write and isSystem pass 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 gained setup.write and an organization, and its engine double gained getObject. The whole file is green.
  • The other census rows: the The write-response internal-field tripwire walks the protocol class only — a direct engine write mouth outside it (rest-server's batch ql.update) is covered by the fix but not by the guard #8497 tripwire (no-record-echo), the objectQLProvider consumer census (6 → 7 sites, all through wiredEngineOrLoud), the query-slot census (0 slots), the docblock floors (7 blocks), and rest-config-mount-table.pin.test.ts (the maintenance row gates four routes).
  • packages/objectql/src/record-currency-backfill.test.ts: a walk cut short by maxRecords says 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 2 prints the incomplete-walk warning.
  • packages/client: the method POSTs the body and unwraps data, and apply is sent only when true; its return type is pinned as unknown.
  • protocol-destructive-currency-mode.test.ts: the remedy names the route.

Acceptance notes

  • The maintenance switch's describe(), with domain:spec consent. packages/spec/src/api/rest-server.zod.ts (L538–541) said the switch mounts GET /meta/diagnostics, GET /meta/_drafts and POST /meta/_migrate-stored, and did not name this door. Commit 12af728f adds POST /meta/_migrate-record-currency-codes in the sentence's own list shape and nothing else under packages/spec/src. It also carries the regenerated content/docs/references/api/rest-server.mdx row and '@objectstack/spec': patch in the card changeset, under domain: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.
  • The SDK method answers unknown, not the report type. Ruling 2C and the seat's amendment keep it untyped like migrateStored. An any return would red the shrink-only packages/client/exported-any-returns.json, so it is unknown. Callers who want the type import it from @objectstack/objectql.
  • The shared envelope, where _migrate-stored answers bare. The new module is born on sendOk / sendError / sendServerFault (check:route-envelope asks new modules for the shared pair). So the report is data, and refusals are { success: false, error: { code, message } }.
  • No actor on the writes. The backfill takes no user. Its writes run under { isSystem: true, tenantId: <current organization> } as the CLI's do, so modified_by is not the admin's id. The door's gate is the attribution point.
  • A tenancy-disabled object (no organization_id) is refused INVALID_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.
  • Commit trailers. Two local commits were first written with a model-named co-author trailer. They were reworded to the model-free pair before any push, so every pushed commit carries Co-authored-by: Claude plus Claude-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 in integration.
  • 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 --noEmit for rest, client, metadata-protocol and objectql: exit 0.
  • Whole-tree turbo run build under 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 by message in record-currency-backfill.ts, and @objectstack/objectql was rebuilt, because rest resolves it through dist. rest-write-route-hook-refusal-sentence.ledger.test.ts then 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/objectstack derived 116 families on head b56a12d6. Each was run with its exit code captured before any pipe.

✓ dispatch-gates --ran: 116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED (a DERIVED zero — all 116 recorded an exit code and none of them is 3).

The first pass, at an earlier head, had one finding: check:system-context-census exited 1, because the door's isSystem capability 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:docs exited 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. After pnpm --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

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec gains one describe string only, at 12af728f.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: one describe hunk, under domain:spec consent 6675431663; the generated docs row comes from gen:docs, and check:generated passes all 14 artifacts.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

…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
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/cli, @objectstack/client, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/rest, @objectstack/spec, touching 42 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

38 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 7da99b79f5fcf3fa1dfc143f39d73fb702dfb271.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • 3 cross-cutting symbol(s) contributed no route anchor: resolveExecCtx (34 routes), routeManager (23 routes), resolveRequestEnvironmentId (4 routes)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 182 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 7da99b79f5fcf3fa1dfc143f39d73fb702dfb271 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5f552fc5951bf2322833ea08f9b87d053ee2e045 — the merge of head 12af728fa8a19542811c866268a2bbb27cd105c4 into base 7da99b79f5fcf3fa1dfc143f39d73fb702dfb271, 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 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

⚠️ 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 7da99b79f5fcf3fa1dfc143f39d73fb702dfb271 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 12af728fa8a19542811c866268a2bbb27cd105c4
Local-runs: none
Scope: packages/spec/src/api/rest-server.zod.ts + content/docs/references/api/rest-server.mdx (the row gen:docs renders from that describe), packages/rest/src/meta-record-currency-backfill-route.ts and packages/rest/src/rest-server.ts (the mount the sentence now names), packages/rest/src/rest-config-mount-table.pin.test.ts (the pin that holds the switch to four routes), .changeset/30675-record-currency-backfill-rest-door.md (the @objectstack/spec patch the consent requires). The rest of the diff is read here too at the lane's own tier (card #30675, ruling 6670707546 item B); the ACCEPT names it.

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 git diff 561d532c4..12af728fa (merge-base of origin/main and the head: 29 files, +1031/−18, the API file list and the local diff agree). Check-runs read through the API at 2026-11-06T04:51Z; nothing built, run or re-run locally.

① Derived judgments

Shape. POST {metaPath}/_migrate-record-currency-codes in a new module meta-record-currency-backfill-route.ts, registered from registerMetadataEndpointsInner (head L5608–5621) beside the object state read, inside the umbrella's route-manager swap, so it sits under the /meta anonymous-deny gate as _migrate-stored does. Body { object, field, code, apply? }, closed; the answer is backfillRecordCurrencyCodes' report as data of the shared envelope. Right.

  • Same switch as _migrate-stored. Yes. The registrar returns on metadata.endpoints.maintenance === false (route L983); _migrate-stored registers under metadata.endpoints.maintenance !== false (meta-collection-routes.ts L315 on main). The two spellings agree for a boolean with default true. The mount-table pin now holds the switch to exactly four routes (_drafts, diagnostics, _migrate-record-currency-codes, _migrate-stored), and the route test pins the sibling switches leave it mounted and the scoped base mounts its twin.
  • Tenant-admin authorized. Yes, as the card fixed it: setup.write in ctx.systemPermissions, isSystem bypassing (route L991–998), asked first, before the body and before the engine, the _migrate-stored order. systemPermissions come from resolveUserAuthzGrants(ql, userId, { tenantId }), so the capability is the one the caller holds in its current organization. manage_metadata alone and setup.access alone are refused 403 (route test; the write-door enumeration's new DATA_DOORS cases on both bases). The isSystem bypass is now owned by row 50b of content/docs/permissions/system-context.mdx, with the regenerated register row. The card left manage_metadata as the alternative only if the maintainer names it; nobody has.
  • Scoped to the caller's organization; the unscoped run refused before any write. Yes. organizationId is ctx.tenantId and nothing else (route L1000); the body may not carry organizationId or allOrganizations (each is 400 VALIDATION_ERROR with its own sentence, L954–956), so no environment-wide form exists on this surface. ctx.tenantId is the session's activeOrganizationId (or the API key's), vetted by vetOrganizationClaim against accessible_org_ids and dropped when the claim fails (resolve-authz-context.ts L624, L779–790 on main). A context with no organization, isSystem included, is 403 before any read (L1001–1009; pinned with the provider never called and zero driver reads). The CLI's refusal of the unscoped multi-organization run therefore has no counterpart to reach: every call is the --org run for one organization, and the test holds that every driver read of the rows carries "organization_id":"org_a" and that B's admin writes b1 and nothing of A's. The backfill itself runs { isSystem: true, tenantId: organizationId } on the walk and the writes, unchanged from the A card.
  • localization.currency read only where D8 allows. It is not read at all. The diff contains no occurrence of localization; the backfill's three mentions on the head are the comments that forbid it (ADR-0144 D8 rule 4, the code is never derived). The SDK docblock says code is the one you give, never derived.
  • The truncated preview says it is truncated. Yes, carried item 2. The JSON report already carried truncated; the shared text no longer prints the "✓ Every … nothing to backfill" line for a capped walk (record-currency-backfill.ts L466–473), says "Walk incomplete", and an unscoped capped run adds that its organization check saw only the rows examined. The CLI prints the warning; REST answers the same report. Pinned in objectql and in the CLI test. Over REST the preview is the JSON report, whose truncated field is documented on the SDK method and in cli.mdx.
  • Report types re-exported. Yes, carried item 1: RecordCurrencyBackfillOrganization and RecordCurrencyBackfillOrganizationsRefusal on the @objectstack/objectql entry, beside the three already there. Type-only, no runtime surface.
  • The objectql hook-refusal fix. In scope and correct. The new door joins the rest-write-route-hook-refusal-sentence.ledger.test.ts population automatically (every write row of REST_ROUTE_LEDGER), so it had to answer the hook's sentence per row; the backfill's failure entry carried the raw err.message, which for a sandboxed hook is the hook NAME threw: debug wrapper. rowFailureSentence (private, not exported) reads the throw as toFailedResult in core's import runner does: a sandbox origin or own fault whose mapDataError verdict is 500 or above takes the door's withheld sentence, a sandboxed refusal takes sandboxBusinessMessage, and anything else keeps its message (the CONTROL test). The operator's logger.warn line keeps the whole text. The CLI's --json report reads the same sentence now, which the dev declared. No new export, no new wire code, no widened surface. The ablation (replace with message, rebuild, 12 ledger rows red; restore, 625 green) is the right proof and is reported with its counts.
  • Envelope and error codes. The dev's two declared deviations hold: sendOk / sendError / sendServerFault for a module born today (check:route-envelope wants the shared pair on new modules), so the report is data and refusals are nested; and the SDK method returns unknown, not any (the shrink-only exported-any-returns.json), pinned in return-type-precision.test.ts. Both are improvements on the _migrate-stored precedent, not drifts from the card. Codes used: FORBIDDEN, VALIDATION_ERROR, NOT_IMPLEMENTED, INVALID_FIELD, OBJECT_NOT_FOUND, INTERNAL_ERROR, all in the registry. A declared 4xx from the body judge or the backfill is answered at its own status and code with details.fields when a field is named; anything else goes through sendServerFault with the route's generic text.
  • Engine access. wiredEngineOrLoud plus a guarded dynamic import('@objectstack/objectql'), the meta-object-state-routes.ts precedent, with the optional-runtime-probe LEDGER row amendment 6675450053 admitted. No engine or no function is 501. objectQLProvider consumer census 6 → 7, all through the seam. The route reads no req.query, so rule 5's closed set does not apply (nor does _migrate-stored read one).
  • Remedy text. The 409 currency_mode_to_record finding now names the route and its body beside the CLI command; the objectql protocol test pins both.
  • Writes carry no actor. modified_by is not the admin's id, as the CLI's writes are; the PR body says so. Acceptable for a twin of the CLI; noted, not a finding.

Observed, not blocking.

  • A non-sandboxed row failure keeps the driver's raw message in failures[].error, which a setup.write holder now reads over REST; the import row passes the same text through sanitizeRowError, which @objectstack/core does not export. The dev noted it (class c, not filed). See ③.
  • The REST_ROUTE_LEDGER row carries no authz field because no conformance cell covers the door yet; the gate is held by the route test and the enumeration test instead. Fine for landing; a conformance cell is follow-up material.

② Semver level

Clause-②: yes (widening) is right: a new route, a new SDK method, two new type exports on the objectql entry, a wider maintenance describe. No consumer loses anything. Metadata-protocol: none is right: no authorable key, value, default or meaning changes; the maintenance describe is a server-config doc string, not an authoring surface. none takes patch, and the changeset gives patch to the six packages the diff moves: rest, client, objectql, cli (the incomplete-walk warning), metadata-protocol (the remedy text) and spec (the describe), with both declaration lines in the body matching the PR body. No (narrowing) arm, no BREAKING banner owed, no ADR-0087 marker owed.

The spec hunk against consent 6675431663, line by line. One file under packages/spec/src: api/rest-server.zod.ts, L538–541. The only change is inside the one describe() string of maintenance: the old tail "GET /meta/_drafts and the POST /meta/_migrate-stored write door" becomes "GET /meta/_drafts, the POST /meta/_migrate-stored write door and the POST /meta/_migrate-record-currency-codes record-currency code backfill", the sentence's own list shape with a serial "and". No key, shape, default or export changes; lazySchema, z.boolean().default(true) untouched. The mount is real in the same PR. content/docs/references/api/rest-server.mdx L228 changes by exactly that row, byte-equal to the concatenated describe (the shape gen:docs renders); no other reference page moves. The changeset carries '@objectstack/spec': patch. check:generated on the head is in the still-running TypeScript Type Check lanes (see counts). The terms are met; no domain:spec review is owed, and this record covers the hunk.

File surface against the claim and its amendments. Inside: packages/rest/** (14 files), the SDK method and its tests (3 files under packages/client/src), the objectql barrel, the remedy text (protocol-write-face.ts) and its objectql test, content/docs/** (4 files, references/api/rest-server.mdx by consent), one root changeset, scripts/check-undeclared-dep-imports.mjs (amendment 6675450053, one LEDGER row), packages/spec/src/api/rest-server.zod.ts (consent 6675431663). Beyond the named surface: packages/objectql/src/record-currency-backfill.ts and its test (the carried truncation item plus rowFailureSentence, the latter declared as a deviation), and packages/cli/src/commands/migrate/record-currency-codes.ts and its test (+21 lines, the incomplete-walk warning, part of the carried truncation item but not named as a surface deviation in the dev report). Nothing governed; ui/**, docs/adr/**, skills/**, .claude/** untouched. Not a fork PR. 1,049 changed lines, under the 3,000 line class.

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, Governed Surface Queue Guard is success; Test Core (8 shards), Build Core, Live E2E UI and the TypeScript Type Check / Lint & Repo Gates lanes are still running; Check Changeset concluded success on this reading. The one failure is PR-body guards (claim · duplicate · Part-of · single-writer path): check:closing-target-claim reads the governing Claim: 6675312689, whose Branch: line still names the placeholder claude/issue-30675-record-currency-backfill-rest; the amendment 6675340866 names claude/project-thread-1lyw0v but is not a Claim:-first-line comment, which is the only shape that guard reads. Not a finding against the diff; a seat act (below).

③ Boundary flags

  • Raw driver text in a per-row failure over REST. failures[].error keeps err.message for a non-sandboxed throw, so a driver's own text (SQL, table or column names) can reach a tenant admin holding setup.write; the import row sanitizes the same class through sanitizeRowError, which core does not export. Fixing it in this PR widens @objectstack/core's surface, outside the claim. Recommendation: file it as a card (the card names its evidence: the import row's rule and this door), not a rider here. No maintainer ruling needed; the seat decides whether to file at ACCEPT.
  • Prose for the seat, no ruling. (a) The PR body's Acceptance notes still call the spec describe hunk "pending spec consent … held back"; consent 6675431663 was given at 2026-11-06T02:43Z and the hunk is pushed as the head commit 12af728. The ACCEPT should say so and quote the consent id, and the body line should be corrected through the relay. (b) The dev's open question on the assignee is moot: the PR carries os-elon-musk as assignee now. (c) check:closing-target-claim stays red until a comment whose first line begins Claim: and whose Branch: line names claude/project-thread-1lyw0v is posted on rest + sdk: tenant-admin door for the record-currency backfill — a REST twin of os migrate record-currency-codes, scoped to the caller's organization (multi-currency, epic #8345, ruling C item B) #30675 by this seat (remedy 1 in the guard's text; the job re-reads the thread live, no push needed). (d) The CLI edits are part of the carried truncation item; the ACCEPT should name them as an accepted surface deviation, since the dev report's deviations list does not.
  • Nothing here needs a maintainer ruling. The gate capability is the card's own choice (setup.write, the ruling's recommendation); manage_metadata remains the alternative only if the maintainer names it on the card.

Implemented-by: claude/project-thread-1lyw0v
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: PASS

Must-fix: none.

Should-fix (for the ACCEPT or a follow-up card, none blocking this head):

  1. File the raw-driver-text pass-through on non-sandboxed row failures as a card, or decide at ACCEPT that an organization admin reading its own organization's driver text is acceptable and say why.
  2. Correct the PR body's Acceptance note on the spec hunk (consent 6675431663, head 12af728).
  3. Re-post the claim on rest + sdk: tenant-admin door for the record-currency backfill — a REST twin of os migrate record-currency-codes, scoped to the caller's organization (multi-currency, epic #8345, ruling C item B) #30675 with Branch: claude/project-thread-1lyw0v so check:closing-target-claim goes green.
  4. A conformance cell for the door, so the ledger row can carry authz.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

席位核对 · ACCEPT · 2026-11-06T05:29Z · #30675

Head-sha: 12af728

  • Shape: 29 files, +1031/−18. The surface:

    • packages/rest: the new POST /meta/_migrate-record-currency-codes door, its mount and route tests, and the write-door census rows.
    • The SDK method in packages/client.
    • packages/objectql: the report types re-exported, the truncated walk says so, and the rowFailureSentence fix.
    • packages/cli: the incomplete-walk warning and its test (+21), which carry the truncation item from ACCEPT 6673657336.
    • The undeclared-dependency LEDGER row (amendment 6675450053).
    • Docs: cli.mdx, currency.mdx, and row 50b in system-context.mdx.
    • The one consented spec describe hunk, with its generated docs row.
    • One changeset, patch on all six packages.

    This sits inside the claim as re-posted (6677151277). Not governed.

  • Contract review: PASS at CONTRACT_REVIEW_TIER, record 6677255440 at this head, with no must-fix. Its findings, confirmed from the diff and main:

    • It shares _migrate-stored's switch. The mount-table pin now holds that switch to four routes.
    • setup.write or isSystem is asked first, against per-organization grants.
    • The organization is the vetted ctx.tenantId only. A body naming organizationId or allOrganizations is 400, and a context with no organization is 403 before any read. So no unscoped run exists over REST, per ruling 6670707546 item B.
    • localization.currency is never read (ADR-0144 D8).
    • A truncated walk says so.
  • Spec hunk: under domain:spec#2's consent 6675431663, term by term:

    • one describe string, in the sentence's own list shape;
    • nothing else under packages/spec/src;
    • the regenerated rest-server.mdx row equals the describe;
    • an @objectstack/spec patch changeset is present.
  • Gates: every check-run on this head has concluded: 65 distinct checks, the latest run of each 55 success, 10 skipped, 0 failure. PR-body guards was red only on the claim's placeholder branch and went green after the full claim was re-posted. Locally (dev report 6677009085):

    • dispatch-gates passed 116/116 at b56a12d6, plus the 14 families on the spec path at this head (check:generated passed all 14 artifacts with a raised local heap).
    • pnpm lint exited 0, and the build was 119/119.
    • Ablation: the rowFailureSentence fix turns 12 ledger rows red without it.
  • Accepted deviations:

    • The SDK method returns unknown, like migrateStored (ruling 2C precedent).
    • The route uses the shared envelope, because check:route-envelope requires it for a new module.
    • The objectql hook-refusal fix: a real leak, private, with no widened surface.
  • Follow-ups filed or recorded:

  • Contract: Clause-②: yes (widening) (a new route, an SDK method and barrel exports) and Metadata-protocol: none, confirmed by the review.

  • Landing: ready, then auto-merge, through the relay. Fixes #30675.

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 size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants