Repository navigation
fix(plugin-audit,cli): activity is written only when an organization is derivable, never as a NULL row; the ceremony deletes the existing NULL rows no anchor attributes (ADR-0131 D9, C8 stage 8b-0c) - #24429
Conversation
…is derivable, never as a NULL row; the ceremony deletes the existing NULL rows no anchor attributes (ADR-0131 D9, C8 stage 8b-0c) Ruling record 6257510973 item 3 (Q3 A). - plugin-audit: the activity mirror takes its organization from the record, the session, then the Default Organization under single. When none is derivable it writes no sys_activity row; the sys_audit_log row is still written. Suites that boot the writer without AuthPlugin now boot the single composition they model (src/__tests__/single-posture-tenancy.ts). - cli: sys_activity's inventory row declares an underivableDeparture. On any posture but single, the plan counts the NULL rows no anchor attributes as departing (and warns), and the apply's mirror-deletion step deletes them after attribution, read from a fresh plan. Under single the Default Organization takes them and nothing is deleted. Claude-Session: https://claude.ai/code/session_018oXkooW8d1rtxYGFYwnAiT Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 34 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 bd31980ac0466c38ebbd97b626171ece364c23e7 && git checkout bd31980ac0466c38ebbd97b626171ece364c23e7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e22defb13c171900e2c1c54d1d15c24ed85fe91b 36af4c053bb716f68ae5687811d0240e3c85b2b5 && git checkout -B drift-repro e22defb13c171900e2c1c54d1d15c24ed85fe91b && git merge --no-ff 36af4c053bb716f68ae5687811d0240e3c85b2b5
node scripts/docs-audit/affected-docs.mjs --json e22defb13c171900e2c1c54d1d15c24ed85fe91b
|
Part of #24343
Part of #15212
Clause-②: no
C8 stage 8b-0c. It executes ruling record 6257510973 item 3 (Q3 A) on #24377: activity is written only when an organization is derivable, and never as a NULL row. #24343 stays open: stages 8b-0d and 8b-0f remain.
What changes
@objectstack/plugin-audit: the activity mirror (audit-writers.ts, the CRUD mirror's derivation).tenancyservice's Default Organization undersingle.sys_activityrow. 8b-0b's?? nullarm is gone.sys_audit_log) is still written, with its D7tenant_idattribution.organization_idcolumn takes the row unstamped, as before.@objectstack/cli: the ownership ceremony.sys_activity's inventory row keeps fate 3 (attribution, with the subject anchor). It now also declares a platform-onlyunderivableDeparture(underivable-activity).single:--applydeletes them.mirror-deletionstep deletes them after attribution. It reads them from a fresh plan, and blocks with nothing deleted if that plan still derives an owner for a NULL row or still reports one.deleted.single, the Default Organization takes those rows, as before, and nothing is deleted.Why a new inventory key and not a
mirror-deletiondeparture. A declared departure is classified before the anchors (walkNullRows), and itswhereruns over every NULL row. Used here, it would delete the rows the subject anchor can attribute, and undersingleevery row the Default Organization takes. The new key is classified after the anchors and the Default Organization. It is platform-only: the spec's supplement row is unchanged (OrganizationOwnershipSupplementEntrySchema), so a host still cannot declare a deletion.organization-ownership-inventory.test.tslists the key with the other platform-only keys.Tests that model
single. 19 plugin-audit suites boot the audit writer without AuthPlugin, with org-less records and sessions. Under this change they would model a wall and write no activity. They now boot thesinglecomposition they test:src/__tests__/single-posture-tenancy.ts, which is thetenancyservice reduced todefaultOrgId(), orgetDefaultOrganizationIdfor the direct installs. Their assertions are unchanged.Measurements (on
origin/main71fa166e)sys_activityrows.git grepoverpackages/**(non-test) finds one insert:audit-writers.tspersistAuditTrailRow,sys.object('sys_activity').create.installActivityStampHookstamps inserts and creates none. An app's own server-side action can still insert into the table directly (sys-activity.object.tsnames hotcrm sites); that path goes through the engine, which is 8b's surface, not this stage's.organization_id = active organizationpredicate excludes NULL, and a row is readable only if its parent record is (activity-read-visibility.ts). No docs page, skill or spec description promises an activity row for an org-less record (grep overcontent/docs,skills,packages/spec/src: 0 hits).Verification (head
36af4c05)pnpm --filter @objectstack/plugin-audit test: 56 files, 898 tests passed. Before the suites bootedsingle, the same run had 160 failures in 19 files, all caused by the missing activity rows.pnpm --filter @objectstack/plugin-audit typecheckandpnpm --filter @objectstack/cli typecheck: exit 0.pnpm --filter @objectstack/cli exec vitest run --project unit: 309 files passed. 2 files failed only on the precondition "packages/cli is not built". Afterpnpm --filter @objectstack/cli buildthey passed (2 files, 29 tests).--project integration), the 15organization-ownership*/apply-organization-ownership-gatesuites undercommands/migrate/andutils/: 13 files passed, 1 skipped; 68 tests passed, 1 skipped.organization-ownership-apply.underivable-activity.test.ts(sqlite fixture):isolated): the plan reports the departure (2 rows) and the warning. The apply attributes 1 row, deletes 2 and leavesremaining 0. The delete runs after the attribution UPDATE. A second run writes nothing to the table.single: 3 rows attributed (2 to the Default Organization), 0 deleted, no warning.packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts(showcase boot,single; the activity reads aboutsys_userrows as an organization member): 12/12 passed.scripts/ablation-replace.mjswith the restore proven (blob equals HEAD,git diff HEADempty):mirrorsActivity && activityOrganizationKnown ?→mirrorsActivity ?: 1 red (writes no activity row under a wall…).walkNullRowsdisabled (false &&prepended): 3 walled cases red.posture !== 'single'arm removed: thesinglecase red.dispatch-gates --ranwith coded exits: 72 accounted for, 70 run, 2 NOT MEASURED (exit 3).check:dual-build-cjs-loadsandcheck:published-readme-exports(no full build);check-plugin-teardown-shape --self-test(shallow clone).check-closing-target-claim,check-partof-closing-keyword,check-single-claim-paths,check-governed-queue-guard.Acceptance notes
derivableActivityOrganization()still catches a throwingtenancy.defaultOrgId()and reads it as "none". Undersinglesuch a throw now drops the activity row; before, it wrote a NULL row. The ledger row is still written. A throwing tenancy service undersingleis not measured to occur.sys_secretor the settings audit (8b-0e), the self-registration grant (8b-0f), or the engine exemption (8b).🤖 Generated with Claude Code
https://claude.ai/code/session_018oXkooW8d1rtxYGFYwnAiT
Generated by Claude Code