Skip to content

test(backup): make survivor-copy retention invariant structural - #824

Merged
EtanHey merged 4 commits into
mainfrom
wt/laneb-retention-guard-core
Sep 9, 2026
Merged

test(backup): make survivor-copy retention invariant structural#824
EtanHey merged 4 commits into
mainfrom
wt/laneb-retention-guard-core

Conversation

@EtanHey

@EtanHey EtanHey commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • pin PR fix(backup): never treat a file as covered without a surviving archive object (S) #815's survivor-copy invariant in the production AST, not only behavior fixtures
  • fail if live inventory, object identity/MD5/source digest, verified-upload control flow, or state-before-delete ordering disappears
  • put the 2026-09-09 incident rationale at both deletion call sites
  • tell deliberate refactors to update the name-coupled guard, never delete it

Why

The July gap was 22 uploaded and verified bundles later deleted by local cleanup and Drive retention, not 22 failed jobs. PR #815 fixes that survivor-copy defect. This stacked PR makes the fix structurally difficult to regress and names the mock-green #815 incident in every failure.

Verification

  • RED-first mutation test: missing checker module
  • RED-first pair-review finding: failure output omitted update-not-delete guidance
  • canonical changed-only gate passed: 23 owned unit, 3 registration, 40 isolated routing, Bun, and FTS shell
  • direct checker passed
  • Claude pair review: ACCEPT; durability finding fixed

Size: M

Stacked on PR #815. Do not merge before its base. Followed by #822 for review-driven semantic hardening.

— brainlayerCodex-4c4386f6 (worker) · codex/gpt-5.6-sol

Note

Add structural AST guard for JSONL backup retention invariant

  • Adds inspect_jsonl_retention_invariant in backup_retention_invariant.py, a static validator that parses the JSONL backup source AST and checks archive existence, byte-digest comparisons, live-inventory propagation, provenance persistence, and verified-upload deletion ordering.
  • Adds a CLI main handler that exits 1 and prints each violation when validation fails, or exits 0 on success; failures include PR fix(backup): never treat a file as covered without a surviving archive object (S) #815 refactor guidance.
  • Adds comments in jsonl_backup.py documenting the ordering and control-flow requirements the guard enforces; no executable statements changed.
  • Adds tests in test_backup_retention_invariant.py and test_jsonl_backup.py verifying failure output with refactor guidance and detecting six targeted source mutations.
  • Risk: the guard rejects any future refactor of jsonl_backup.py that weakens the checked invariants; refactors that move or rename retention-related functions will need to update the guard first.

Macroscope summarized 3b6bff4.


Note

Medium Risk
Touches backup durability enforcement and name-coupled AST checks; production backup behavior is unchanged, but refactors to run_backup must update the guard or CI will fail.

Overview
Adds backup_retention_invariant.py, an AST-based CI guard that keeps PR #815’s surviving-copy contract in jsonl_backup.py: live Drive inventory before coverage, strict _state_matches checks, provenance persisted before prune/local unlink, and deletions only under result["verified"] and upload. Violations print update-not-delete guidance referencing PR #815.

Tests assert the real module passes the guard and that six deliberate weakenings of production source each fail with the expected message; a small test covers the CLI failure output.

jsonl_backup.py gains incident-focused comments at inventory fetch and deletion sites, pointing reviewers at the guard—no runtime logic changes in this PR.

Reviewed by Cursor Bugbot for commit 3b6bff4. Bugbot is set up for automated code reviews on this repo. Configure here.

EtanHey and others added 3 commits September 9, 2026 21:08
Keep PR #815 survivor identity, byte checks, call ordering, and deletion control flow under a mutation-tested CI guard. Put the 2026-09-09 retention incident at each dangerous call site.

Co-Authored-By: brainlayerCodex-4c4386f6 running gpt-5.6-sol <noreply@anthropic.com>
Co-Authored-By: brainlayerCodex-4c4386f6 running gpt-5.6-sol <noreply@anthropic.com>
Co-Authored-By: brainlayerCodex-4c4386f6 running gpt-5.6-sol <noreply@anthropic.com>
@EtanHey EtanHey added the size:M Tight-loop PR size: 151-400 hand-written lines changed label Sep 9, 2026
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 5d90320f-6180-43f6-8403-146ad1e627c0

📥 Commits

Reviewing files that changed from the base of the PR and between 0443e60 and 3b6bff4.

📒 Files selected for processing (4)
  • src/brainlayer/backup_retention_invariant.py
  • src/brainlayer/jsonl_backup.py
  • tests/test_backup_retention_invariant.py
  • tests/test_jsonl_backup.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtanHey

EtanHey commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Please focus on whether this core static guard is actually reached by changed-only mapping, whether each required survivor-copy/deletion-order invariant fails independently when removed, and whether the #815 update-not-delete guidance is actionable.

— brainlayerCodex-4c4386f6 (worker) · codex/gpt-5.6-sol

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T18:40:41.895296Z 692df53 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@EtanHey
EtanHey changed the base branch from wt/p0-retention to main September 9, 2026 18:37
@EtanHey
EtanHey marked this pull request as ready for review September 9, 2026 18:38
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

BrainLayer ratchet

Every Value below was measured by this run. A row this machine cannot measure says n/a — <reason> instead of a number; baselines in Notes name their own machine, method and date and were not measured here.

Row Status Value (measured by this run) Method Notes
commit provenance 🟢 GREEN measured 3b6bff41743e == PR head · checkout 85fdfc7e0214 commit graph + live PR head · in-process · runner Which commit this whole table is about. On a pull_request event the checkout is GitHub's synthetic merge ref, whose sha is not on the PR — #759's table printed 13fa724278bf while that PR's head was 4632f979 — so this row names the PR-head parent instead, the sha a reviewer can actually see. The comparison sha is read live from repos/{owner}/{repo}/pulls/{n} when the table is collected, not taken from the event payload, because the payload cannot know the run has been overtaken. Residual window, stated rather than papered over: a push landing between that read and the comment being posted is not caught here — the run for that push refreshes the table.
baseline attestation 🟢 GREEN baseline f421d1a7c5e6 matches the main attestation (run 34389821246 · main 0443e603fb69 · 2026-09-09T18:34:34Z) main attestation artifact via Actions API · in-process · runner What every comparison is measured AGAINST, and who says so. The baseline fields of tests/fixtures/sprint_gate/corpus.json (queries, latency_baseline_ms, thresholds) are compared to the ratchet-attestation artifact of the latest successful push or (no-input) workflow_dispatch run of ratchet-attest.yml on main, fetched through the Actions API — a PR run cannot write to another run's artifacts. A field that differs is RED unless that main run measured the new value. The calibrated socket collector can license p50/p95; every absent measured path stays locked, so missing collection never passes as permission for a hand edit. Boundary: the comparator is this PR's checkout of ci_ratchet_table.py, diff-reviewable, not tamper-proof.
provenance 🟢 GREEN stamped 85fdfc7e0214 == HEAD, tree clean wheel stamp · in-process · runner Sha half of #749 keg-mode provenance: a keg built from this wheel can answer __build_sha__. The helper-age and served-process predicates need a running BrainBar and are measured only by scripts/sprint_gate.py on an installed Mac. The sha here is the checkout's — the merge ref on a PR — because that is what publish.yml stamps at release time; the PR-head sha this table describes is the one in commit provenance above.
fallback replay debt ⚪ n/a n/a — no fallback queue on this machine: the pending memories live in ~/Gits/*/docs.local/decisions, and docs.local/ is gitignored, so a runner checkout has no copy of them to count docs.local walk · machine with the fallback queue intended_brain_store: true with no chunk_id means a memory reached disk and never reached the DB, so it answers no brain_search. Budget: 0. Any pending or unparseable file is a finding, never a band -- 122 of these sat from 2026-06-28 to 2026-09-05 because nothing counted them where a reader would look. Measured by walking the tree, so it is only ever measured on a machine that HAS the tree.
mapped bytes ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac Baseline 26.2 GB — installed Mac, socket, 2026-09-03, after R2 drained 15,070 → 0. Up from 16.8 GB because the drain left more vectors mapped under the same cap: the change is the drain, not a leak. Not measured by this run.
search p50/p95 ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac Margin p50: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin p95: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Calibrated on MacBook-Pro.local at 2026-09-01T08:42:22Z under active_sprint_load (tests/fixtures/sprint_gate/corpus.json). Not measured by this run.
idle CPU ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would ps sampling · installed Mac Ceiling: average CPU < 30% over a 60 s window (resource_budget in scripts/sprint_gate.py), ratified and kept as a hard budget. Margin daemon: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin helper: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin watcher: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Needs the BrainBar daemon, helper and watcher actually running. Not measured by this run.
signature_valid ⚪ n/a n/a — the macOS signature-parity job is trigger-gated and did not run on this PR: it touches no release or signing path (pyproject.toml, scripts/release-*, scripts/brainlayer-version-check.sh, publish.yml, ratchet.yml) and carries no ratchet:signatures label — a GitHub macOS runner bills at ~10× Linux minutes and rebuilds the keg venv from source codesign · installed keg scripts/release-verify-signatures.sh <keg> codesign-verifies every *.so/*.dylib under libexec/venv. The macOS parity job installs the published tap formula (etanhey/layers/brainlayer), so this row measures the release path — formula, published sdist and Homebrew's relocation — and not this PR's tree. Release-time baseline for the same keg on a different machine: 442 valid / 0 invalid — installed Mac (M4 Max), brew --prefix brainlayer 1.5.11, 2026-09-03.

🟢 GREEN measured, within budget · 🔴 RED measured, out of budget — a finding to clear before merge · ⚪ n/a not measurable on this machine, never guessed.

No RED rows.

Measured on Linux/x86_64 · measured 3b6bff41743e · PR head 3b6bff41743e · checkout 85fdfc7e0214 · run · updated 2026-09-09 18:38:53 UTC

@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_223b4ff0-3c65-4ded-acf7-6933f2f2d483)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 692df53e3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4 to +8
def test_jsonl_retention_guard_failure_tells_refactors_to_update_not_delete(tmp_path: Path, capsys) -> None:
from brainlayer.backup_retention_invariant import main

unsafe_path = tmp_path / "jsonl_backup.py"
unsafe_path.write_text("def run_backup():\n pass\n", encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Map guard changes to the production mutation test

When a changed-only push modifies only src/brainlayer/backup_retention_invariant.py, the generic source mapping selects this matching test file, but this test only feeds main() a stub with missing functions; it never runs the production-source and mutation assertions in test_jsonl_backup.py. Weakening or removing one of the checker's detailed invariant checks can therefore pass the normal focused gate, so this owner needs to exercise those assertions as well.

AGENTS.md reference: AGENTS.md:L225-L227

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in stacked #822 commit 9f61371b. The matching changed-only owner now executes the full production-source mutation test. Verified with BRAINLAYER_CHANGED_FILES=src/brainlayer/backup_retention_invariant.py: the owner test ran and the entire changed-only gate passed.

— brainlayerCodex-4c4386f6 (worker) · codex/gpt-5.6-sol

Comment on lines +140 to +144
state_write_calls = _calls(run_backup, "_update_state_for_uploaded")
required_state_keywords = {"archive_id", "archive_md5", "digests"}
if not state_write_calls or not any(
required_state_keywords <= {keyword.arg for keyword in call.keywords if keyword.arg}
for call in state_write_calls

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Track the durable state write before deletion

When a refactor retains _update_state_for_uploaded(...) before pruning but moves or removes its enclosing _atomic_write_json, this code still treats the helper call as a state write and reports no error. For example, replacing _atomic_write_json with print leaves inspect_jsonl_retention_invariant() returning [], even though Drive pruning and local unlinking can now happen without durable survivor provenance; the ordering check must follow the actual persistence call.

AGENTS.md reference: AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in stacked #822. Commit 2bcf4eff tracks the direct _atomic_write_json persistence call and exact positive deletion gate; later review-driven commits add independent mutations for ordering, both deletion calls, inventory flow, early success, producer starvation, and predicate neutralization. #824 is the core layer; #822 is the mandatory hardening layer and will be retargeted to main before the preserved #824 branch is deleted.

— brainlayerCodex-4c4386f6 (worker) · codex/gpt-5.6-sol

Comment on lines +68 to +72
condition = ast.unparse(node.test)
if 'result["verified"]' not in condition and "result['verified']" not in condition:
continue
if "upload" not in condition:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate the verified-upload predicate semantically

When the deletion condition is inverted to if not result["verified"] and not upload:, both required substrings are still present, so this helper marks the enclosed prune and unlink calls as verified-upload control flow and the checker returns clean. That permits deletion under exactly the unsafe conditions the guard is meant to reject; inspect the boolean expression for positive conjuncts rather than searching its rendered text.

AGENTS.md reference: AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in stacked #822. Commit 2bcf4eff tracks the direct _atomic_write_json persistence call and exact positive deletion gate; later review-driven commits add independent mutations for ordering, both deletion calls, inventory flow, early success, producer starvation, and predicate neutralization. #824 is the core layer; #822 is the mandatory hardening layer and will be retargeted to main before the preserved #824 branch is deleted.

— brainlayerCodex-4c4386f6 (worker) · codex/gpt-5.6-sol

Comment on lines +53 to +57
'if result["verified"] and upload:',
"if upload:",
"backup deletion calls must remain inside verified-upload control flow",
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exercise every deletion invariant independently

The mutation list ends after testing one combined verified-upload condition and never independently removes or reorders the live-inventory listing, the selector-to-predicate inventory handoff, either deletion call, or persistence relative to each deletion. Those corresponding checker branches can therefore be deleted while this test's positive assertion and all six mutations remain green, defeating the intended structural protection even in a full-suite run.

AGENTS.md reference: AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in stacked #822. Commit 2bcf4eff tracks the direct _atomic_write_json persistence call and exact positive deletion gate; later review-driven commits add independent mutations for ordering, both deletion calls, inventory flow, early success, producer starvation, and predicate neutralization. #824 is the core layer; #822 is the mandatory hardening layer and will be retargeted to main before the preserved #824 branch is deleted.

— brainlayerCodex-4c4386f6 (worker) · codex/gpt-5.6-sol

Comment on lines +174 to +177
def main(argv: list[str] | None = None) -> int:
args = sys.argv[1:] if argv is None else argv
path = Path(args[0]) if args else Path("src/brainlayer/jsonl_backup.py")
errors = inspect_jsonl_retention_invariant(path.read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit refactor guidance when the inspected file moves

When a deliberate refactor renames or deletes jsonl_backup.py, Path.read_text() raises before the checker can produce REFACTOR_GUIDANCE; similarly, deleting the checker makes the new owner test fail at import time. Thus the exact delete/rename scenarios targeted by “UPDATE this guard; do not delete it” yield only a raw exception rather than the actionable instruction, so missing-path/module handling must live outside the artifact being protected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requested case is covered: every structural mismatch emits the #815 incident and says UPDATE this guard; do not delete it. Deleting the checker artifact itself still fails the matching changed-only owner at import time; a module cannot emit guidance after it has been deleted. Turning that raw missing-artifact failure into custom prose would require an additional outer guard and is not part of this core invariant.

— brainlayerCodex-4c4386f6 (worker) · codex/gpt-5.6-sol

@EtanHey
EtanHey merged commit 286024f into main Sep 9, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M Tight-loop PR size: 151-400 hand-written lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant