Skip to content

Commit 5a916c4

Browse files
claude[bot]os-samclaude
authored
Backfill the org-less platform rows the #10101 writers no longer produce — dry run first, idempotent (#11439)
* feat(approvals): plan the org-less platform-row backfill — dry run only The DRY-RUN half of the #11308 one-off repair sweep, landed on its own so the write path cannot be what defines the plan: for a script that touches existing data the dry run has to exist first, and be a deliverable in its own right. `planPlatformRowOrganizationBackfill` reads only. It scans each stranded platform table for rows whose organization column is unset, re-reads each row's SUBJECT record, and reports — broken out per object — what it would write, plus the rows it deliberately would not: a subject with no organization of its own is counted and NAMED (out of the ruling), never given an invented one. Both the column read on the subject and the column written on the platform row come from the shared `createRecordOrganizationResolver`, so `sys_api_key`'s stamp-only `active_organization_id` fork is honoured rather than flattened. Refs #11308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4 * feat(approvals): write the org-less platform-row backfill, on the plan the dry run printed The write half, on top of the dry run rather than beside it: `applyPlatformRowOrganizationBackfill` takes the PLAN a human already read and issues one update per planned row carrying its id and its resolved organization column — nothing else on the row, which is what makes the undo expressible as "write NULL back to these ids". `runPlatformRowOrganizationBackfill` defaults to `dryRun: true`; writing is opt-in. A row whose update throws is recorded on its object's plan and the sweep continues — a driver rejecting one row must not cost the other N-1 their repair, and a half-done sweep is safe because the next run picks up exactly what is still unstamped. Idempotency is asserted, not claimed: the suite runs the sweep twice against the same engine and pins the second run at zero planned and zero written, with the deliberately-skipped rows re-REPORTED at the same count. Refs #11308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4 * test(approvals): hold the backfill's fake engine to the platform's own write contracts Two gate findings, both real and both about the DOUBLE rather than the sweep: - its `update()` was looser than `ObjectQL.update` — now opened with `assertEngineUpdateDispatch(data, options)` from `@objectstack/metadata-core`, and the pinned ledger learns the file; - its WHERE matcher read a `$`-combinator as a field name, and sat inside a closure the conformance gate could not lift. It now refuses the combinators it does not implement, at module scope where the gate can judge it. Refs #11308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4 --------- Co-authored-by: os-sam <sam@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent a41069b commit 5a916c4

4 files changed

Lines changed: 1085 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/plugin-approvals": patch
3+
---
4+
5+
**Ops:** a one-off, idempotent backfill for the platform rows the pre-#10101 writers stranded with no organization — dry run first (#11308).
6+
7+
#10101 fixed the WRITERS: a `sys_approval_request` and a `sys_automation_run` are now stamped from the SUBJECT record's organization, with the acting context as the ruled fallback. It wrote nothing to existing rows, so the population produced before it persists — a **pending** org-less approval request LOCKS the record it is about while being invisible in every organization-scoped inbox, its own owner's included, and automation-run history stays unattributed. This is the repair for those rows, on the maintainer's 2026-08-23 ruling (direction 3).
8+
9+
`packages/plugins/plugin-approvals/src/backfill-platform-row-organizations.ts` sweeps `sys_approval_request` (with its `sys_approval_action` / `sys_approval_approver` children, which move with their request) and `sys_automation_run`. It scans only rows whose organization column is unset, re-reads each row's subject at repair time — live record first, the write-time snapshot (`payload_json` / `context_json`'s `record`) second for a subject that has since been deleted — and stamps the platform row with the subject's own organization.
10+
11+
**Dry run first, and by default.** `planPlatformRowOrganizationBackfill(engine)` reads only and returns a per-object report naming every row it would touch; `runPlatformRowOrganizationBackfill(engine, { dryRun: false })` writes. Nothing runs at boot and nothing is scheduled: this is an operator-invoked module, run once against an affected install.
12+
13+
**Rows whose subject is equally org-less are counted and named, never written.** The acting-context fallback the writers apply is not available to a repair — the acting context is gone — and inventing one stays vetoed. Those ids are reported so the population is checkable and stays visible.
14+
15+
**`sys_api_key`'s divergence is preserved, not flattened.** Both the column read on a subject and the column written on a platform row are resolved from the registered schema through the shared `createRecordOrganizationResolver` (`@objectstack/metadata-core`), so a platform row ABOUT an API key is repaired from that object's stamp-only `active_organization_id` (limb 0, #8778) and the credential table itself is never written to.
16+
17+
**Idempotent, and asserted rather than claimed.** Every scan is `WHERE <organization column> IS NULL` and every write fills that column, so a repaired row cannot match again; the test suite runs the sweep twice and pins the second run at zero writes.
18+
19+
Publishes no runtime code: the module is not exported from the package index and not bundled into `dist` (`tsup` builds `src/index.ts`). It is graded rather than skipped because the release notes are where an operator of an affected install learns the repair exists, what it will and will not touch, and that the dry run comes first.

0 commit comments

Comments
 (0)