You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while implementing #13533 (PR #14528), which removed the isSystem skips from bindRuleHooks so a system write now materialises sharing-rule grants per record, exactly as a user write does. That is correct and was the maintainer's ruling. This card records the one system-write bulk path the census turned up that the hook layer cannot help, so it is triaged on its own rather than folded into the fix.
The shape
claimSeedOwnership (packages/plugins/plugin-security/src/claim-seed-ownership.ts) hands seeded business records to the first platform admin. For every non-sys_, non-managedBy, non-external object that declares owner_id, it runs two scans at limit: 10_000 and then writes each matched id as its own single-id update under { isSystem: true }:
Up to 20,000 ids per object, one write each. Since #13533 every one of those writes on a rule-covered object fires afterInsert/afterUpdate and therefore one evaluateAllForRecord pass.
Why the existing batch tail does not reach it
rule-hooks.ts already has the trailing-batch machinery the #13533 ruling asks for — a write whose row set is unbounded or over RULE_RECOMPUTE_ROW_CAP (1000) takes revokeThenQueueRegrant into evaluateAllRulesForObject. That branch is reached by looking at ONE write's row set. Here each write's row set is legitimately a single row, so the cap never fires: the batch exists only in the caller's loop, which the hook layer cannot see. Making the hooks coalesce across writes is not the answer either — it would make the approval write-back eventually-consistent, which is the defect #13533 just fixed.
Actual exposure, stated honestly
Bounded, and smaller than the raw numbers suggest:
runBootstrap fires at kernel:ready, so the boot pass runs before plugin-sharing's kernel:bootstrappedbackfillRuleGrants — which reconciles every rule anyway. The per-record work at boot is therefore duplicated, not load-bearing.
The pass is also self-limiting: it only matches rows whose owner_id is null or usr_system, so after the first pass there is nothing left to claim.
runBootstrap is ALSO replayed at runtime through an ObjectQL middleware (shouldReplayBootstrapFor, security-plugin.ts), and that replay has no trailing backfill behind it — but by then the rows are claimed, so the realistic row count is ~0.
So the worst case is a fresh install with a large seeded, rule-covered dataset paying N per-record sharing evaluations at boot that the boot backfill then repeats. Nothing is incorrect; the grants converge either way.
Possible dispositions, for triage rather than pre-judged
Leave it. The duplication is one-time per boot and the numbers above may make it irrelevant in practice. Cheapest, and honest if nobody has a slow boot.
Make the writer batch: turn the loop into a predicate write per object, which both cuts the write count and lets the existing cap/trailing-batch branch do its job with no change to plugin-sharing.
Measured while implementing #13533; not reproduced against a timed boot, so there is no latency number here — that measurement is part of whichever disposition triage picks.
Found while implementing #13533 (PR #14528), which removed the
isSystemskips frombindRuleHooksso a system write now materialises sharing-rule grants per record, exactly as a user write does. That is correct and was the maintainer's ruling. This card records the one system-write bulk path the census turned up that the hook layer cannot help, so it is triaged on its own rather than folded into the fix.The shape
claimSeedOwnership(packages/plugins/plugin-security/src/claim-seed-ownership.ts) hands seeded business records to the first platform admin. For every non-sys_, non-managedBy, non-externalobject that declaresowner_id, it runs two scans atlimit: 10_000and then writes each matched id as its own single-id update under{ isSystem: true }:Up to 20,000 ids per object, one write each. Since #13533 every one of those writes on a rule-covered object fires
afterInsert/afterUpdateand therefore oneevaluateAllForRecordpass.Why the existing batch tail does not reach it
rule-hooks.tsalready has the trailing-batch machinery the #13533 ruling asks for — a write whose row set is unbounded or overRULE_RECOMPUTE_ROW_CAP(1000) takesrevokeThenQueueRegrantintoevaluateAllRulesForObject. That branch is reached by looking at ONE write's row set. Here each write's row set is legitimately a single row, so the cap never fires: the batch exists only in the caller's loop, which the hook layer cannot see. Making the hooks coalesce across writes is not the answer either — it would make the approval write-back eventually-consistent, which is the defect #13533 just fixed.Actual exposure, stated honestly
Bounded, and smaller than the raw numbers suggest:
runBootstrapfires atkernel:ready, so the boot pass runs before plugin-sharing'skernel:bootstrappedbackfillRuleGrants— which reconciles every rule anyway. The per-record work at boot is therefore duplicated, not load-bearing.owner_idis null orusr_system, so after the first pass there is nothing left to claim.runBootstrapis ALSO replayed at runtime through an ObjectQL middleware (shouldReplayBootstrapFor,security-plugin.ts), and that replay has no trailing backfill behind it — but by then the rows are claimed, so the realistic row count is ~0.So the worst case is a fresh install with a large seeded, rule-covered dataset paying N per-record sharing evaluations at boot that the boot backfill then repeats. Nothing is incorrect; the grants converge either way.
Possible dispositions, for triage rather than pre-judged
plugin-sharing.sharing-plugin.ts(ruleGrantsBootReconciled). Deliberately NOT done in 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533: a phase flag that never gets set would silently reinstate exactly the defect that card fixed, so it wants its own decision rather than riding along.Measured while implementing #13533; not reproduced against a timed boot, so there is no latency number here — that measurement is part of whichever disposition triage picks.
Generated by Claude Code