Skip to content

lifecycle loadGovernance: a failed sys_organization read silently drops EVERY tenant retention override, so a deletion policy runs on partial evidence #12853

Description

@claude

Found while triaging the parameterless-catch read seams for #12753 (see PR #12851 for the full 11-site triage). Filed unassigned, and ⛔ with no domain:* — that is triage's single-producer field, per the convention recorded on #8901.

⛔ Not repaired in #12851: that PR's scope is a gate-header record, and this is a source fix in another package.

The seam

packages/objectql/src/lifecycle/lifecycle-service.ts, loadGovernance():

if(typeofengine.find==='function'&&declared.length>0){try{constorgs=awaitengine.find('sys_organization',{limit: TENANT_SCAN_LIMIT,context: { ...SYSTEM_CTX}});for(constorgoforgs??[]){ ... snapshot.tenantOverrides.set(objectName,list);}}catch{// No sys_organization (single-tenant kernel) - tenant overrides n/a.}}returnsnapshot;

Why it is wrong

Same #4728 shape as the seam this is filed beside: the comment names one benign cause, the catch swallows every cause. But the consequence here is heavier than an invisible row, because the value being computed governs deletion.

snapshot.tenantOverrides is the ADR-0057 §3.2 per-tenant retention/expiry window set. On any failure of the sys_organization read — connection loss, permission refusal, a driver fault — the map is returned empty, and the sweep that consumes it cannot tell "this deployment has no tenant overrides" from "the read that would have found them did not happen". Every tenant then falls back to the global retention window, which is the wrong window in both directions: a tenant configured to keep rows LONGER has them expired early, and one configured to keep them shorter keeps them.

The failure is not reported anywhere: loadGovernance returns a GovernanceSnapshot with no field saying the tenant pass was incomplete, and the catch logs nothing.

The fix

}catch(error){if(!isMissingTableError(error))throwerror;// packages/metadata/src/errors.ts}

— or, if the sweep must stay fail-open on every branch, one logger.error naming the consequence, which is what the read-seam rule asks for at a seam that genuinely cannot discriminate. What must not stay is an empty override map that is indistinguishable from a configured one.

Measurement context

1 of 11 read seams with a parameterless catch on origin/main @ 6f0fec3d0, and 1 of only 2 of those 11 graded genuinely wrong. Related but distinct: #8906 (closed) covered checkGovernance in the same file — a different method, a different probe. Not covered by #8901's cohort, #8896 or #8895.


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions