Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/lifecycle-governance-tenant-scan-read-failure.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
---
"@objectstack/objectql": patch
---

fix(objectql): a failed `sys_organization` tenant scan no longer decides a retention window (#12853)

`LifecycleService.loadGovernance()` filled `snapshot.tenantOverrides` — the
ADR-0057 §3.2 per-tenant retention/expiry window set — behind a bare `catch {}`
whose comment named ONE benign cause ("No sys_organization (single-tenant
kernel)") while the `catch` swallowed every cause. On a connection drop, a
timeout, a permission refusal or a driver fault the map came back EMPTY, and an
empty map is not a neutral value: `reap()` and `archiveObject()` read it as
"this deployment has tuned no tenant" and fall every tenant back to the global
window. That window is wrong in both directions, and the expensive direction is
a tenant configured to retain LONGER having its rows expired early. Nothing
reported it: `GovernanceSnapshot` carries no field saying the tenant pass did not
complete, and the catch logged nothing — so the platform deleted on knowingly
incomplete evidence, without knowing the evidence was incomplete.

The scan now discriminates by error TYPE through the shared
`isMissingTableError` predicate. An unprovisioned `sys_organization` really does
mean "no tenant overrides", so a single-tenant kernel is unchanged. Every other
cause aborts the sweep **before any policy is applied** — for a deletion action,
"do not act on incomplete evidence" is the correct failure direction, and a log
cannot bring back a reaped row. The rows a deferred sweep leaves are still there
for the next one.

Operational posture change, deliberate and worth stating: a transient
`sys_organization` outage now costs a sweep. The abort is REPORTED, not thrown —
one `report.errors` entry per declared object plus a `warn` — because `sweep()`'s
declared contract is that it never throws and the scheduler enters it as
`void this.sweep()`, where a rejection would be unhandled. That is the same
objection #8906 recorded when it declined to rethrow from `checkGovernance` one
method below.

Bump argued, not defaulted: `patch`. No exported signature, type, option or
report field moves — the failure surfaces through `LifecycleSweepReport.errors`,
which already exists for exactly this. The tension is honest and does not change
the answer: what a deployment observes on a failure path DOES change (a sweep
that used to complete silently now aborts and says so), but that is the
correction of a defect, not a new capability, and the sibling repairs in this
family (#8896, #8906, #9817) all shipped as `patch`.
30 changes: 30 additions & 0 deletions .changeset/seed-loader-sole-organization-read-failure.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/metadata-protocol": patch
---

fix(metadata-protocol): a failed `sys_organization` probe is no longer answered as "no sole organization" (#12852)

`SeedLoaderService.resolveSoleOrganizationId()` sat behind a bare `catch {}` whose
comment named ONE benign cause — "sys_organization may not exist (single-tenant
runtime)" — while the `catch` swallowed every cause. A dropped connection, a
timeout, a permission refusal or a driver fault all arrived at the caller as
`undefined`, which is not a neutral value here: it is the verdict the method's
own JSDoc calls "genuinely ambiguous", so `load()` stamped no `organization_id`
and every BUSINESS seed row of that run landed org-less — invisible afterwards
under strict org-scoping. Nothing reported it either: `SeedLoadResult` carries an
`errors` field and this path never touched it, so the operator saw a clean,
successful seed.

The repair is the one already landed on the sibling probe across the engine
boundary (`ObjectQL.probeInstallOrganizations`, #9817), copied: bind the
parameter and ask the declared predicate. Only an unprovisioned TABLE is
truthful emptiness — the exact cause the swallowed comment already named — so
the JSDoc's "or when `sys_organization` is absent" stays true, while every other
cause now propagates with its envelope intact.

Bump argued, not defaulted: `patch`. No exported signature, type or option
moves, and the declared answer for every case the JSDoc describes is unchanged.
What changes is a failure path — a seed run that used to complete while writing
invisible rows now fails loudly — which is the correction of a defect rather
than a new capability. The three landed repairs in this family (#8896, #8906,
#9817) all shipped as `patch`, and this is the site that pass missed.
Loading
Loading