Skip to content

Commit 7d483e1

Browse files
Elon Muskclaude
andauthored
fix(objectql): the Archiver resolves its window through P4 governance (#10528) (#10864)
`archiveObject` read `archive.after` — and, since #10347, `ttl.expireAfter` — straight off the declaration, so for any object declaring `lifecycle.archive` an operator's settings override was silently ignored, a registered `LifecycleRetentionFloor` was never evaluated, and per-tenant windows did not apply. The cause is structural, not a forgotten call: `reapObject` returns into `archiveObject` when `archive` is declared, so the three `effectiveWindowMs` resolutions on the reap path sit on a branch archive-declaring objects skip entirely. Governance is therefore resolved inside `archiveObject`, where the #10347 ruling already put the decision about WHICH window is due — resolving it in the caller would duplicate that selection or split one decision across two methods, and the per-tenant leg is a pass over this method's own batch loop either way. All three legs now run through the same resolver the Reaper uses: the global override (on the key matching the selected window), the retention floor (#5195) for both overrides and declarations, and per-tenant windows as one candidate read per overriding tenant plus a global pass covering NULL-org rows. #10347's cutoff selection, the retain-first posture, the per-batch abort checks and the cold-side `archive.keep` prune are unchanged. Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM Co-authored-by: Claude <noreply@anthropic.com>
1 parent cfb8ae4 commit 7d483e1

3 files changed

Lines changed: 680 additions & 53 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
The Archiver resolves its window through ADR-0057 P4 governance (#10528).
6+
`LifecycleService.archiveObject` read `archive.after` — and, since #10347,
7+
`ttl.expireAfter` — straight off the declaration, so for any object declaring
8+
`lifecycle.archive` an operator's settings override was silently ignored, a
9+
registered `LifecycleRetentionFloor` was never evaluated, and per-tenant windows
10+
did not apply.
11+
12+
This was not a forgotten call. `reapObject` **returns** into `archiveObject` for
13+
any object declaring `archive`, so the three `effectiveWindowMs` resolutions on
14+
the reap path sat on a branch archive-declaring objects skip entirely — which is
15+
why the divergence was total rather than partial, and why threading an override
16+
into the cutoff alone would still have left floors and tenant windows unreached.
17+
18+
All three legs now run, through the same resolver the Reaper uses:
19+
20+
- a per-object `retention_overrides` entry beats the declaration, on the key that
21+
matches which window the selection picked — `expireAfter` for a ttl-selected
22+
archive, `maxAge` for an age-selected one;
23+
- an override below a registered floor is rejected (the declared window stands),
24+
logged at `error` naming the registrar, consequence and fix, and recorded in
25+
`report.floorViolations` — the leg whose absence was *silent*, since an empty
26+
`floorViolations` is indistinguishable from a healthy sweep. A *declared*
27+
archive window below a floor is reported the same way and still enforced;
28+
- tenant-scoped windows issue one candidate read per overriding tenant, then one
29+
global pass covering everyone else including NULL-org rows — the shape `reap()`
30+
already used, with tenant overrides going through the same floor.
31+
32+
Unchanged on purpose: #10347's cutoff **selection** (a declared `ttl` still
33+
decides which rows move, on `ttl.field`); the retain-first posture (no archive
34+
datasource ⇒ `archive-pending`, hot-delete only what the cold store took); the
35+
per-batch abort checks, now the first act of every pass; and the cold-side
36+
`archive.keep` prune, which bounds the archive rather than the hot store and has
37+
no settings key. An object with no override and no floor sweeps exactly as
38+
before, as one pass over exactly the predicate it ran before.

0 commit comments

Comments
 (0)