Skip to content

Commit 286eb9e

Browse files
docs(qa): lifecycle-retention-sweep steps[4] re-expects the idle sweep's one aggregate line (#23169) (#23652)
Fixes #23169 Clause-②: no ## What This is the last open row of the family card: `platform-core.lifecycle-retention-sweep` steps[4], the idle-proof step. It is re-expected under the maintainer's ruling A: decision 6147446653, answered 「23169 我同意。」, recorded in comment 6160866003. The checklist step was stale, and the product is correct. One file and one item change: `docs/qa/platform-checklist/areas/platform-core.json`, item `platform-core.lifecycle-retention-sweep`. Its revision goes from 2 to 3, with one appended `history` entry. - **steps[4]** read "idle proof: … the log carries NO sweep line — an idle sweep logs nothing". It is now a positive heartbeat check. One more sweep runs on the now-clean DB: a second interval on steps[2]'s boot, or a fresh boot plus 90 s with `OS_LOG_LEVEL=info` as in steps[2]. That sweep still logs its one aggregate INFO line, `[lifecycle] sweep: N policy(ies) applied, ~0 rows reaped, …`, and the reaped rows stay gone. - N counts the policies applied, not rows. - On an info-level boot, a missing line means no sweep reached its print. It never means "idle". - A boot at the default warn level never prints the line, so it proves nothing. - **fixtures.knownGaps[3]**, the "RULING OWED" entry, now reads "RULED, no longer a gap" and keeps its reason. That follows the README rule that a closed gap keeps its reason, and the RULED / CLOSED / FIXED prefixes other items use. A run record scored against revision 2 or earlier stays interpretable. - **Unchanged:** - steps[5], the kill switch, which is still bracketed by steps[2]'s armed control; - every acceptance clause; - `negative`, `variants`, `traps`, `title` and `source`. - No clause rested on steps[4] before, and none does now. No product change: `packages/objectql` is not touched. ## Grounds, re-read at `c5dd7fce3` - In `packages/objectql/src/lifecycle/lifecycle-service.ts`, `sweep()` prints the aggregate line when `report.swept.length > 0 || report.errors.length > 0 || report.alerts.length > 0`. - `reap()` runs `report.swept.push({ object, class: lc.class, policy, cutoff, deleted: total })` for every applied policy, including a `total` of 0. - The rotation leg pushes its entry unconditionally. - An archive-pending object goes to `report.skipped`, not to `swept`. - A `deleted: 0` swept entry is pinned in `lifecycle-service.no-tenant-column.test.ts`. - **Attribution, corrected from the ruling record:** - "one aggregate log line per sweep is the entire trace it leaves" is the code comment at that print site. - ADR-0057 §3.3's own text says the service's own deletes and rotations "are not audited or activity-logged", and "Aggregate one summary row at most." - The item and its history entry attribute each wording to where it actually lives. ## Method The edit is one span-scoped scripted edit in Node, against BASE `c5dd7fce3`. Each anchor (steps[4], knownGaps[3], the item-level `revision` line, the history tail) was asserted to occur exactly once, both in the file and in the item's span. After the write, the script asserted on disk that: - every byte outside the item's span is identical to BASE; - the 29 other items parse identical to BASE; - the item changed only in `fixtures`, `steps`, `revision` and `history`, and inside those only in `knownGaps[3]` and `steps[4]`; - the revision went from 2 to 3, and `history` is the base's 2 entries plus 1. ## Verification at head `cad70efd0` `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 13 commands for 1 path. `--ran` printed "13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN". - **Exit 0, 12 gates:** - check-ci-filter-parity; - check-closing-keyword-parity, and its `--self-test`; - check-comment-mask-corpus; - check:cross-package-test-inputs; - check:doc-authoring; - check:driver-memory-census; - check:gitlink-declared; - check:nul-bytes; - check:refd-timer-probe; - check:watch-hint-literal; - `@objectstack/lint` check:doc-formula-expressions, after building the lint dependency closure under the verify lock. - **`pnpm check:platform-checklist` exits 1 with "8 problem(s)", at BASE and at head alike.** - The sorted problem lines diff empty, and none is in `platform-core`. - All 8 sit in `access-security`, `attachments-storage` and `search`, which belong to the in-flight work of #23593. They are pre-existing and not touched here. - **`--anchor-census`** is identical at BASE and head: 724/741 anchors, plus 10 on the named residual. `platform-core.json` holds 69 on both. - **`node scripts/checklist-select.mjs platform-core.lifecycle-retention-sweep`** returns 1 runnable item. - **A control-byte self-scan** of the edited file found none. **`skip-changeset`:** this is QA data outside every package. - `git grep 'docs/qa' -- '*package.json'` exits 1, and the root package is private. - Positive control: `packages/spec/package.json` carries a `files` list. There is no ablation or reverse verification: the diff is ledger text, and no tested behaviour moved. ## Acceptance notes - **The ruling record misattributes one quote.** Comment 6160866003 and decision 6147446653 quote "one aggregate log line per sweep is the entire trace it leaves" as ADR-0057 §3.3. It is the code comment at the print site. The item and its history entry carry the correction; the comments themselves are not edited. Carrier: this PR. Not filed. - **No test pins the INFO aggregate line itself.** The report shape is pinned, `deleted: 0` included; the print gate and its message are not. A future change that silenced idle sweeps would turn steps[4] red only on the next live run. Observation only; 承接者:无. Not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6792be6 commit 286eb9e

1 file changed

Lines changed: 9 additions & 3 deletions

File tree

‎docs/qa/platform-checklist/areas/platform-core.json‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,7 +2418,7 @@
24182418
"title": "ADR-0057 lifecycle sweep: backdated telemetry rows past retention are reaped BOUNDED under a system context on the hourly sweep, onlyWhen scoping spares live rows, an archive-declaring object is NEVER hot-deleted unarchived (retained + reported archive-pending when no archive datasource exists), the transient class stays on the primary, and OS_LIFECYCLE_DISABLED=1 disarms the sweep",
24192419
"since": "v16",
24202420
"status": "active",
2421-
"revision": 2,
2421+
"revision": 3,
24222422
"priority": "P1",
24232423
"surface": "mixed",
24242424
"personas": ["operator (local shell + direct sqlite access)", "seeded admin (API reads)"],
@@ -2433,15 +2433,15 @@
24332433
"the ARCHIVE-SUCCESS leg (rows copied to the archive datasource, then hot-deleted) needs a registered 'archive' datasource, which no stock boot has — that leg rides the unit pin (lifecycle-service.test.ts) and stays blocked(fixture) live; what IS live on stock is the SAFETY side: sys_audit_log rows older than 90d are RETAINED and the sweep reports the object skipped archive-pending",
24342434
"class-routing to a dedicated 'telemetry' datasource (ADR-0057 §3.6) engages only when a telemetry datasource is registered — absent on stock, so the routing leg is asserted at the unit/source level (engine.ts getDriver priority + SYSTEM_LEDGER_LIFECYCLE_CLASSES), not live",
24352435
"the ≤ REAP_BATCH_SIZE × REAP_MAX_BATCHES_PER_SWEEP bound (500×20 per object per sweep) is proven by the unit suite; staging >10k backdated rows live is not required — the live clause asserts the reap happened via the bounded per-id path (the aggregate log line), not the bound's edge",
2436-
"RULING OWED on the idle-proof step, steps[4] (QA run #23166 C1, parked on #23121, carried by #23169): an idle sweep on a clean DB still logs '[lifecycle] sweep: 18 policy(ies) applied, ~0 rows reaped, …' at objectstack 36b2889c, and the source at 6d7f4df3c reads the same. The print gate tests report.swept.length, and the reaper pushes a report.swept entry for every policy it applies, deleted 0 included (lifecycle-service.ts; the LifecycleSweepReport.swept docblock reads 'Policies applied, one entry per (object, policy)'). So steps[4]'s 'an idle sweep logs nothing' is either a stale step (one line per sweep is the design) or a product defect (the line should print only when rows moved). Until the maintainer rules, a run records the line verbatim and scores steps[4] neither pass nor fail. No acceptance clause rests on it"
2436+
"RULED, no longer a gap — the idle-proof step, steps[4] (QA run #23166 C1, parked on #23121, carried by #23169): the maintainer ruled (decision 6147446653, ruling A recorded in 6160866003) that the step was stale and the product is correct, and revision 3 rewrites steps[4] as a positive heartbeat check. The line the run saw is the design: an idle sweep on a clean DB logs '[lifecycle] sweep: 18 policy(ies) applied, ~0 rows reaped, …' (objectstack 36b2889c), because the print gate in lifecycle-service.ts tests report.swept.length and the reaper pushes a report.swept entry for every policy it applies, deleted 0 included. ADR-0057 §3.3 bounds that trace ('Aggregate one summary row at most'); the one-line-per-sweep wording is the code comment at the print site, not ADR text. A run scored against revision 2 or earlier that saw this line on an idle sweep observed correct behaviour, not a fail"
24372437
]
24382438
},
24392439
"steps": [
24402440
"boot once on the file DB so schema exists; stop the server",
24412441
"hand-seed via sqlite: (a) N sys_automation_run rows with created_at 40 days ago, status 'completed' (past the 30d window, terminal); (b) 2 rows created_at 40 days ago with status 'paused'/'running' (past the window but NOT terminal); (c) 1 sys_audit_log row created_at 100 days ago (past archive.after 90d); (d) 1 sys_inbox_message row (transient class). Record exact counts and ids",
24422442
"boot again (control boot, no lifecycle flag) with OS_LOG_LEVEL=info exported (or `--log-level info`): the aggregate sweep line is logged at INFO, and `os dev` defaults the kernel logger to warn, so a default-level boot never prints it; wait ≥90s; grep the log for '[lifecycle] sweep: … policy(ies) applied, ~… rows reaped …' (the ONE trace a sweep leaves — cleanup must not re-feed the tables it drains)",
24432443
"re-read via API/sqlite: the terminal 40d-old automation rows are GONE; the paused/running 40d-old rows SURVIVE (onlyWhen scoping); the 100d-old audit row SURVIVES and the sweep report lists sys_audit_log skipped archive-pending (no 'archive' datasource registered → retained, never dropped unarchived); the inbox row's fate follows its own declared policy only — the transient class got no cross-class treatment",
2444-
"idle proof: a second sweep interval later (or a fresh boot on the now-clean DB + 90s), the log carries NO sweep line — an idle sweep logs nothing (the line only prints when something was swept/errored/alerted)",
2444+
"idle heartbeat (ruled on #23169: an idle sweep still logs its line): let one more sweep run on the now-clean DB — keep steps[2]'s boot up for one more sweep interval (hourly, DEFAULT_LIFECYCLE_SWEEP_MS), or boot fresh on the same file DB with OS_LOG_LEVEL=info exported as in steps[2] and wait ≥90s. That sweep still logs its ONE aggregate INFO line, '[lifecycle] sweep: N policy(ies) applied, ~0 rows reaped, …': the line prints whenever a policy was applied, and every applied policy counts, deleted 0 included, so N counts (object, policy) pairs, not rows, and does not fall to 0 when nothing is due (with 0 error(s) it equals steps[2]'s N on the same composition; the archive-pending sys_audit_log is reported skipped and never counted). Record the line verbatim, then re-read: the rows steps[3] saw reaped stay gone and its survivors still survive. A missing line on an info-level boot means no sweep reached its print (not armed, disabled in settings, or aborted — e.g. the '[lifecycle] governance snapshot could not be loaded' warn), never 'idle'; a default warn-level boot never prints the line at all, so it proves nothing here",
24452445
"kill switch: re-seed one terminal 40d-old automation row; boot with OS_LIFECYCLE_DISABLED=1 AND OS_LOG_LEVEL=info (the same level as the control boot, or the missing line proves nothing); wait ≥90s; the row SURVIVES and no sweep line appears — and this absence is meaningful only because step 2 proved the sweep active on the same composition and log level (absence-inference guarded)",
24462446
"run the unit pin for the legs live cannot stage: pnpm --filter @objectstack/objectql exec vitest run src/lifecycle/lifecycle-service.test.ts — the archive-declared skip ('skips hot deletion entirely while an archive is declared (retain → archive → delete)', report.skipped == [{object, reason: 'archive-pending'}]), the bounded-reap cases, and the rotation fallback"
24472447
],
@@ -2527,6 +2527,12 @@
25272527
"date": "2026-10-13",
25282528
"change": "steps[2] (the control boot) and steps[5] (the kill-switch boot) now boot with OS_LOG_LEVEL=info, and a knownGap records the open ruling on steps[4], the idle proof (stale step + ruling owed, #23166 C1; parked on #23121, carried by #23169). The '[lifecycle] sweep:' line is logged at INFO, and `os dev` defaults the kernel logger to warn (packages/cli/src/commands/dev.ts --log-level, serve.ts), so the grep in steps[2] could never match on a default boot, and the kill-switch boot must match its level for its missing line to mean anything. steps[4] is not rewritten: an idle sweep still logs 'N policy(ies) applied, ~0 rows reaped', because report.swept carries one entry per applied policy and the print gate tests its length. Whether that is the design or a defect is the maintainer's call; the knownGap says how a run scores it meanwhile. No acceptance clause changed",
25292529
"ref": "#23169"
2530+
},
2531+
{
2532+
"revision": 3,
2533+
"date": "2026-10-15",
2534+
"change": "the idle-proof ruling is applied: decision 6147446653, answered 「23169 我同意。」 and recorded as ruling A in 6160866003 (the checklist step was stale; the product is correct). steps[4] is rewritten from 'an idle sweep logs nothing' to a positive heartbeat: one more sweep on the now-clean DB, at OS_LOG_LEVEL=info as in steps[2], still logs its one aggregate line with ~0 rows reaped, and the reaped rows stay gone. Grounds, re-read at objectstack c5dd7fce3: the print gate in packages/objectql/src/lifecycle/lifecycle-service.ts tests report.swept.length (or errors, or alerts), and reap() pushes a report.swept entry for every applied policy, deleted 0 included (a deleted: 0 entry is pinned in lifecycle-service.no-tenant-column.test.ts); an archive-pending object goes to report.skipped instead. Attribution, corrected from the ruling record: 'one aggregate log line per sweep is the entire trace it leaves' is the code comment at that print site, while ADR-0057 §3.3's own text is that the service's deletes and rotations are not audited or activity-logged, and 'Aggregate one summary row at most'. fixtures.knownGaps[3], which recorded the owed ruling, now reads RULED and keeps the reason, so a run record scored against an earlier revision stays interpretable. steps[5] (the kill switch, still bracketed by steps[2]'s armed control), every acceptance clause, negative, variants and traps are unchanged: no clause rested on steps[4] before, and none does now. No product change",
2535+
"ref": "#23169"
25302536
}
25312537
]
25322538
},

0 commit comments

Comments
 (0)