Skip to content

Commit 86c0aea

Browse files
committed
docs(adr-0057): reap guards amendment + attachments changeset (#2755)
Document the LifecycleService reap-guard seam (domain re-verification + external-resource reclaim) as an ADR-0057 amendment, with sys_file as the first consumer, and add the release changeset for the framework packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0187NT3Qer9oep5dCRb9b8Lt
1 parent 54f09fb commit 86c0aea

2 files changed

Lines changed: 60 additions & 0 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': minor
3+
'@objectstack/service-storage': minor
4+
'@objectstack/platform-objects': minor
5+
'@objectstack/verify': minor
6+
'@objectstack/rest': patch
7+
---
8+
9+
feat(attachments): sys_file orphan lifecycle + parent-derived attachment access (#2755)
10+
11+
**Orphan lifecycle (ADR-0057).** Deleting a `sys_attachment` join row used to
12+
orphan the backing `sys_file` row and its storage bytes forever. `sys_file`
13+
now declares a lifecycle (`ttl 30d` on a new `deleted_at` tombstone for
14+
orphans; `retention 7d onlyWhen status=pending` for abandoned uploads), the
15+
storage plugin's new hooks tombstone a file when its LAST join row is deleted
16+
(attachments scope only — `Field.file`/`Field.image`/avatar scopes are never
17+
touched) and un-tombstone on re-attach, and a new LifecycleService **reap
18+
guard** seam (`registerReapGuard`) re-verifies zero references at sweep time
19+
and deletes the storage bytes before confirming each row reap. A guarded
20+
object is never blind-deleted; an erroring guard fails safe (rows retained).
21+
22+
**Attachment access (ADR-0049, Salesforce parent-derived semantics).**
23+
`sys_attachment` create now requires caller READ visibility of the parent
24+
record (403 `ATTACHMENT_PARENT_ACCESS`) and server-stamps `uploaded_by` from
25+
the session (client value ignored); delete requires uploader-or-parent-editor
26+
(403 `ATTACHMENT_DELETE_DENIED`). The storage upload routes require an
27+
authenticated session when an auth service is wired (401 `AUTH_REQUIRED`;
28+
bare kernels stay open) and stamp `owner_id` on new files.
29+
30+
**REMOVED — `sys_attachment.share_type` / `sys_attachment.visibility`.**
31+
Both fields were modeled in v1 with zero runtime consumers (ADR-0049
32+
parsed-but-unenforced). There is no replacement key: attachment access is
33+
derived from the parent record by the hooks above. Writers of these fields
34+
should simply stop sending them (unknown-field validation will reject them);
35+
existing DB columns are left as unmanaged leftovers, no migration needed.
36+
37+
`@objectstack/verify` gains `BootOptions.extraPlugins` for booting optional
38+
service pairs (e.g. storage + audit) in dogfood fixtures.

docs/adr/0057-system-data-lifecycle-and-retention.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,28 @@ registered ⇒ rows are retained (today's behavior), reported as
170170
`archive-pending` — a compliance ledger cannot be destroyed by declaring a
171171
lifecycle.
172172

173+
#### Amendment (#2755): reap guards — domain re-verification + external-resource reclaim
174+
175+
Some reapable rows are pointers to state the Reaper cannot see: `sys_file`
176+
rows reference storage bytes (disk/S3), and a tombstoned orphan may have
177+
regained `sys_attachment` references since it was marked. For these, a plugin
178+
may register a **reap guard** at runtime
179+
(`lifecycle.registerReapGuard(object, guard)`): the Reaper fetches candidate
180+
rows in batches, the guard confirms each id — performing external cleanup
181+
(byte deletion) and sweep-time re-verification first — or vetoes it (kept,
182+
retried next sweep). Rules:
183+
184+
- A guarded object is **never blind-deleted**: an erroring guard fails safe
185+
(rows retained), and an engine without row reads skips the object
186+
(`reap-guard-unsupported`) rather than degrading to a blind delete.
187+
- Guards are runtime wiring, not spec surface — detection and scheduling stay
188+
inside the single platform sweep (no bespoke sweepers, per the alternatives
189+
below); the guard is a domain callback, not a scheduler.
190+
- First consumer: `sys_file` (service-storage) — attachment orphans are
191+
tombstoned by hooks when their last `sys_attachment` reference is deleted,
192+
reaped `30d` later by TTL with byte reclaim, with zero-reference
193+
re-verification at sweep time; abandoned `pending` uploads reap after `7d`.
194+
173195
### 3.4 Reclaim — driver space hygiene
174196

175197
SQLite driver defaults to `auto_vacuum=INCREMENTAL` (shipped P0); the Reaper

0 commit comments

Comments
 (0)