Skip to content

fix(fallback-replay): refuse to mark the real files against a DB that is not canonical (XS) - #780

Merged
EtanHey merged 4 commits into
mainfrom
wt/replay-d-marker-db-guard
Sep 5, 2026
Merged

fix(fallback-replay): refuse to mark the real files against a DB that is not canonical (XS)#780
EtanHey merged 4 commits into
mainfrom
wt/replay-d-marker-db-guard

Conversation

@EtanHey

@EtanHey EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Beyond the three-PR brief. I found this doing #774's live-check, so per canon rule 9 it opens its own PR rather than riding on that branch. Close it if the lane does not want it — but read the demonstration first, because the trap is laid across the ratified live-check procedure itself.

Stacked on #774 (both touch the same script). Retarget to main once that lands.

The defect

The marker write is not DB-scoped. src/brainlayer/fallback_replay.py never reads a DB path at all — grep -n "db_path\|DEFAULT_DB_PATH\|canonical" returns nothing. replay_entry stores, then writes chunk_id into the file under --gits-root, whatever --db pointed at.

So the shape the ratified procedure asks for — a copy of the DB — permanently stamps every pending file in the real tree with an id that exists only in a throwaway DB. is_pending_entry then answers "not pending", and those memories are hidden with nothing left to say they were never stored.

Demonstrated on a scratch tree against a brand-new empty DB:

REPLAYED .../demo.md -> STORED manual-a282a4652bf84ce2
file now carries    chunk_id: manual-a282a4652bf84ce2
rows in the CANONICAL DB with that id: 0
inventory pending_count: 1 -> 0        # the debt is now invisible

The plan for this work says "Test against a COPY of the real DB first", and the 2026-08-02 ratification requires it for anything touching stored data. Following that instruction with this tool, as shipped, silently hides every memory it touches. I only avoided it in #774 and #777 by also copying the fallback files — which nothing told me to do and nothing required.

The fix

marker_target_hazard fails closed on exactly the mismatch that hides data: a non-canonical --db while --gits-root resolves to the real ~/Gits. Both honest shapes stay allowed:

--db --gits-root
copy copy of the files ✅ a live-check
canonical real tree ✅ the production drain
copy real tree ⛔ exits 2, says why
canonical copy ✅ (harmless)

The queued path is guarded the same way — it writes queued_chunk_id into the same files.

A silence next to it

_emit never printed result["error"] in text mode, so a refusal and a clean run emitted the same three count lines and only the exit code told them apart. That already applied to the two pre-existing early exits (--legacy requires queued replay and the --limit refusal), which were equally invisible. Text mode now leads with ERROR: <reason>.

Verified against the live tree

$ ... --gits-root ~/Gits --db <copy> --apply --direct-db-write --limit 200
ERROR: refusing to replay: --db `...livecheck/replay-a.db` is not the canonical DB
(`...brainlayer.db`), but --gits-root is the real tree `/Users/etanheyman/Gits`, so this run
would mark the real fallback files as replayed with chunk ids that exist only in that DB —
hiding those memories with no trace. Copy the fallback files too and pass --gits-root <copy>,
or use the canonical DB.
exit code: 2

$ inventory ~/Gits
pending_count: 122 legacy: 0      # nothing was marked

Test that fails on the base commit

Four, in tests/test_fallback_replay.py. The two guard tests fail on the base (main() == 0, no refusal); the two allow-tests pass on the base and must keep passing, which is what stops the guard from blocking the production drain. Measured before implementing.

Scope

XS. 38 hand-written source lines, 149 test lines. Read-only against the canonical DB; no production fallback file was written at any point.

ruff check and ruff format --check clean. Scoped pre-push; tests/test_fallback_replay.py 49 passed.


Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])


Note

Medium Risk
The script marks production docs.local fallback files; a guard regression could either block the real drain or fail open and silently hide pending memories again.

Overview
Adds a fail-closed safety gate to replay_brain_store_fallbacks.py for --apply: before inventory runs, marker_target_hazard blocks replay when --db is not the canonical DB (via get_canonical_db_path, not env-affected DEFAULT_DB_PATH) while --gits-root still targets the real ~/Gits tree—including subtrees, symlinks, and case aliases detected with os.path.samefile. Unsafe runs exit 2 with an explicit refusal; copy DB + copy files and canonical DB + real tree stay allowed for live-check and production drain. The same guard covers queued replay because it also writes markers into fallback files.

Refactors main into helpers for legacy parsing, queue replay, and direct DB replay. Text-mode _emit now prints ERROR: for refusals and other result["error"] cases. Tests add broad coverage for refuse/allow shapes, guard ordering, and end-to-end production drain wiring.

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

Note

Refuse noncanonical DB apply against the real Gits tree in replay_brain_store_fallbacks

  • Adds an early apply-mode guard in main that checks marker_target_hazard before scope loading or fallback inventory, returning status 2 on unsafe combinations.
  • The guard treats the real home Gits root, its subtrees, symlink aliases, and case-insensitive path aliases as the real tree, and allows only the canonical DB (resolved via get_canonical_db_path) with it.
  • Refactors main by extracting legacy parsing, queued replay, and direct replay into separate helpers; adds _emit printing of an ERROR: line in text mode for refusals and replay failures.
  • Adds extensive tests covering refusal and allowlist shapes, subtree/symlink/case aliases, guard ordering, and end-to-end production drain.
  • Behavioral Change: unsafe apply invocations that previously proceeded now exit early with status 2 and a refusal message; safe copy/copy and canonical-DB/real-tree combinations are unaffected.

Macroscope summarized 9d36a60.

@cursor

cursor Bot commented Sep 5, 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_57133060-3c83-4f92-804c-0559dd37116f)

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 34 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: Team

Run ID: cab232ad-2986-41a5-841e-2b52d42c4ea9

📥 Commits

Reviewing files that changed from the base of the PR and between 2448231 and 9d36a60.

📒 Files selected for processing (2)
  • scripts/replay_brain_store_fallbacks.py
  • tests/test_fallback_replay.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 added the XS Extra-small change (400 lines or fewer) label Sep 5, 2026
@EtanHey

EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Focus, in order:

  1. Is the guard's predicate the right one? It keys on --gits-root resolving to Path.home()/"Gits". That catches the real tree wherever it is reached from, but it does NOT catch a symlink farm or a bind mount pointing at the same files under a different path. Tell me if there is a cheap way to compare the actual trees instead.
  2. Fail-closed direction. A non-canonical DB with the real tree is refused; the canonical DB with a COPY of the files is allowed as harmless. Is that second case actually harmless, or can it hide something too?
  3. DEFAULT_DB_PATH is read as a module global so a test can patch it. Flag it if that makes the guard bypassable in a way that matters.
  4. The deeper fix would be to make the marker itself DB-scoped — record which DB a replay went into, and treat a marker from a non-canonical DB as still pending. I kept this PR to an entry-point guard to stay XS. Say if you think the schema change is the right call instead.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

@deepsource-io

deepsource-io Bot commented Sep 5, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 2448231...9d36a60 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Sep 5, 2026 5:15p.m. Review ↗
Swift Sep 5, 2026 5:15p.m. Review ↗
JavaScript Sep 5, 2026 5:15p.m. Review ↗
Shell Sep 5, 2026 5:15p.m. Review ↗
Secrets Sep 5, 2026 5:15p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== PR metadata and changed files ==='
gh pr view 780 --repo EtanHey/brainlayer --json baseRefName,headRefName,files,title,url

echo '=== PR diff ==='
gh pr diff 780 --repo EtanHey/brainlayer -- src/brainlayer/fallback_replay.py tests/test_fallback_replay.py

echo '=== fallback_replay outline ==='
ast-grep outline src/brainlayer/fallback_replay.py --items all

echo '=== test outline ==='
ast-grep outline tests/test_fallback_replay.py --items all

echo '=== marker and pending references ==='
rg -n -C 4 --glob '*.py' \
  '\b(chunk_id|queued_chunk_id|is_pending_entry|DEFAULT_DB_PATH|replay_entry|queued)\b' \
  src tests

Length of output: 1517


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== PR diff ==='
gh pr diff 780 --repo EtanHey/brainlayer

echo '=== script outline ==='
ast-grep outline scripts/replay_brain_store_fallbacks.py --items all

echo '=== test outline ==='
ast-grep outline tests/test_fallback_replay.py --items all

echo '=== script: relevant definitions and calls ==='
rg -n -C 12 \
  '^(def (marker_target_hazard|_emit|main|replay_entry)|DEFAULT_DB_PATH|CANONICAL|queued_chunk_id|chunk_id|is_pending_entry)' \
  scripts/replay_brain_store_fallbacks.py

echo '=== tests: guard and marker-related cases ==='
rg -n -C 8 \
  '^(def test_.*(hazard|canonical|marker|queued)|.*marker_target_hazard|.*DEFAULT_DB_PATH|.*queued_chunk_id|.*chunk_id)' \
  tests/test_fallback_replay.py

Length of output: 56763


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== replay script main ==='
sed -n '1,170p' scripts/replay_brain_store_fallbacks.py

echo '=== fallback replay public and marker functions ==='
ast-grep outline src/brainlayer/fallback_replay.py --items all
rg -n -C 10 \
  '^(def (is_pending_entry|inventory_fallbacks|replay_entry|queue_entry|mark_fallback_stored|_write_replay|_write_queue)|.*\b(chunk_id|queued_chunk_id)\b)' \
  src/brainlayer/fallback_replay.py

echo '=== Python version declarations ==='
rg -n -C 2 'python_requires|requires-python|target-version|python_version|Python ::' \
  pyproject.toml setup.cfg setup.py tox.ini .github 2>/dev/null || true

echo '=== Existing filesystem identity checks ==='
rg -n -C 3 --glob '*.py' \
  'samefile\(|samestat\(|stat\(\).*st_(dev|ino)|st_(dev|ino).*stat\(' \
  .

Length of output: 38616


@EtanHey I found one safety gap in the guard predicate.

Path.resolve() detects a --gits-root symlink that resolves directly to ~/Gits. It does not detect a bind mount. It also does not detect a mixed tree where a repository or a directory inside an otherwise copied tree points at the real files. Those forms can still receive marker writes through a non-canonical DB.

A cheap stronger check is available. After inventory, compare each pending marker target with its corresponding path below ~/Gits:

canonical_path = real_gits_root / entry.path.relative_to(gits_root)
if canonical_path.exists() and entry.path.samefile(canonical_path):
    # This marker target is a real fallback file.

Path.samefile() compares filesystem identity. It detects directory symlinks and bind-mounted files on normal local filesystems. Run this check for every structured and legacy entry that can receive a marker. Refuse if any target matches and --db is non-canonical. This also covers a symlink farm at the granularity that matters: the file that would be rewritten.

A root-level samefile() check alone is not sufficient. It misses a copied root that contains mounted or linked real repositories.

The allowed direction is harmless for the stated failure mode:

  • Canonical DB + copied files can write a marker only into the copied files.
  • The real fallback files remain pending.
  • A later production replay can store, merge, or deduplicate the real pending entry and then mark the real file.

This shape can add an extra write attempt to the canonical DB. It cannot hide the real fallback debt. I would keep it allowed.

The patchable DEFAULT_DB_PATH does not create a meaningful production bypass. The module global is mutable only for an in-process Python caller that can already import and modify the script. The normal CLI cannot change it. The test seam is reasonable. A small improvement is to pass the canonical path explicitly into marker_target_hazard() if you want to make the dependency visible, but I do not consider that required for this PR.

A DB-scoped marker is the stronger long-term fix. It would make the invariant local to is_pending_entry() instead of dependent on this CLI entry point. It requires a stable DB identity, updates to marker writers, inventory semantics, queue-drain semantics, and migration behavior for existing markers. The entry-point guard is appropriate for this XS fix, provided it checks actual marker targets rather than only the root path.

I recommend blocking on the per-entry identity check.

You are interacting with an AI system.

Comment thread scripts/replay_brain_store_fallbacks.py Outdated
Comment thread tests/test_fallback_replay.py Outdated
entry = parse_fallback_file(_pending_file(repo, "docs.local/decisions/copy.md"))

class DummyStore:
def close(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

Comment thread tests/test_fallback_replay.py Outdated
entry = parse_fallback_file(_pending_file(repo, "docs.local/decisions/prod.md"))

class DummyStore:
def close(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

EtanHey added a commit that referenced this pull request Sep 5, 2026
…(round 1)

Round-1 review, #780. All five findings real; the two HIGHs were measured on the
unfixed guard before touching it.

**@183 (HIGH) — exact equality failed open on every subtree.**

    exact   ~/Gits            -> REFUSED
    subtree ~/Gits/brainlayer -> ALLOWED     <- the hole

`--gits-root ~/Gits/<repo>` with a copy DB still inventories and marks the
production `docs.local` files under that repo: the same silent hide the PR exists to
stop. A guard whose whole job is to fail closed cannot have a shape that fails open.
Now `is_relative_to`, with both sides `resolve()`d so a symlinked or `..`-laden path
cannot dodge it, and an unresolvable path refuses rather than passes. After:

    exact / subtree / .worktrees deep      -> REFUSED, REFUSED, REFUSED

**@185 (MEDIUM) — the allowlist was the env, not the disk.** `paths.DEFAULT_DB_PATH`
is `resolve_db_path()` evaluated at import, so it becomes whatever `BRAINLAYER_DB`
says. Measured with `BRAINLAYER_DB=/tmp/copy.db`:

    DEFAULT_DB_PATH       = /tmp/copy.db                            <- allowlisted!
    get_canonical_db_path = ~/.local/share/brainlayer/brainlayer.db

So the copy became "canonical" and the trap reopened through the env instead of
through `--db`. `canonical_db_target()` now reads `get_canonical_db_path()`, and the
refusal message no longer calls an env override canonical.

**@1362 (HIGH) — the production-allow test never reached the branch it claimed.** It
passed a tmp gits-root, so the guard returned None before the DB was consulted and
the `DEFAULT_DB_PATH` monkeypatch was dead. A regression that always refused on the
real tree — blocking the production drain outright — still got `main() == 0`. The
test now fakes `Path.home()` so real-tree + canonical-DB is genuinely exercised, and
patches `get_canonical_db_path` so the whole chain runs, not a shim.

**@1305 (MEDIUM) — the refusal path walked the real tree on its way to saying no.**
`main()` called `load_scope_map` + `inventory_fallbacks` before the guard. The guard
now runs first, and a test makes both of those raise if reached, proving the refusal
touches nothing.

**@1282 (LOW) — both fail-open shapes now have tests**, plus a deeper-subtree case.

Also: moving the guard earlier pushed `C901 main is too complex` from main's
pre-existing 11 to 13. Rather than worsen a metric DeepSource already fails on, the
two replay engines are extracted into `_replay_via_queue` / `_replay_direct_to_db`
(same code, same order, only nesting changes) and the guard receipt into
`_refuse_marker_hazard`. C901 is now GONE from this file — below the threshold for
the first time, clearing a failure that predates this lane.

Smoke-tested both engines after the extraction: queued path -> 3 DEFERRED, 3 queue
files; direct path -> 1 STORED. Live-verified the guard on this machine, and `~/Gits`
still reports pending_count 122 — nothing marked.

tests/test_fallback_replay.py 60 passed. Rebased onto #774's round-1 commit
(conflict was two additive test blocks; both kept).

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey
EtanHey force-pushed the wt/replay-d-marker-db-guard branch from a8e647d to 345d1ee Compare September 5, 2026 15:48
@cursor

cursor Bot commented Sep 5, 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_199422ac-551c-447a-9752-d7591bae0a60)

Comment thread tests/test_fallback_replay.py Outdated
entry = parse_fallback_file(_pending_file(repo, "docs.local/decisions/copy.md"))

class DummyStore:
def close(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

Comment thread tests/test_fallback_replay.py Outdated
entry = parse_fallback_file(_pending_file(repo, "docs.local/decisions/prod.md"))

class DummyStore:
def close(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

@EtanHey

EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Round 1 — all five fixed. New head 345d1ee1 (rebased on #774's round-1 commit).

Both HIGHs were measured on the unfixed guard before I touched it, so these are receipts, not readings.

@183 (HIGH) — exact equality failed open on every subtree

exact   ~/Gits             ⇒ REFUSED
subtree ~/Gits/brainlayer  ⇒ ALLOWED     ← the hole

You are right that this is the whole point of the PR inverted: --gits-root ~/Gits/<repo> with a copy DB still inventories and marks the production docs.local files under that repo. A guard whose only job is to fail closed cannot have a shape that fails open.

Fixed with is_relative_to, both sides resolve()d so a symlinked or ..-laden path cannot dodge it, and an unresolvable path refuses rather than passes (cannot prove safe, so not safe). After:

exact ~/Gits                          ⇒ REFUSED
subtree ~/Gits/brainlayer             ⇒ REFUSED
deep ~/Gits/brainlayer/.worktrees/x   ⇒ REFUSED

@185 (MEDIUM) — the allowlist was the env, not the disk

BRAINLAYER_DB=/tmp/copy.db
  DEFAULT_DB_PATH        = /tmp/copy.db                            ← allowlisted!
  get_canonical_db_path  = ~/.local/share/brainlayer/brainlayer.db

Exactly as you said: paths.DEFAULT_DB_PATH is resolve_db_path() evaluated at import, so an env-selected copy became canonical and the trap reopened through BRAINLAYER_DB instead of --db. canonical_db_target() now reads get_canonical_db_path(), and the refusal message no longer calls an env override canonical.

@1362 (HIGH) — the allow-test never reached the branch it claimed

Confirmed: a tmp gits-root meant the guard returned None at the first check, so the DEFAULT_DB_PATH monkeypatch was dead and a regression that always refused on the real tree — blocking the production drain outright — still got main() == 0. The test now fakes Path.home() so real-tree plus canonical-DB is genuinely exercised, and patches get_canonical_db_path so the full chain runs rather than a shim.

@1305 (MEDIUM) — the refusal walked the real tree on its way to saying no

Right. The guard now runs before load_scope_map / inventory_fallbacks, and a test makes both raise if reached, so the refusal path provably touches nothing.

@1282 (LOW) — both fail-open shapes now have tests, plus a deeper-subtree case.

One thing I did beyond the findings

Moving the guard earlier pushed C901 main is too complex from main's pre-existing 11 to 13. Rather than worsen a metric DeepSource already fails on, I extracted the two replay engines into _replay_via_queue / _replay_direct_to_db (same code, same order, only nesting changes) and the receipt into _refuse_marker_hazard. C901 is now gone from this file entirely — below threshold for the first time.

Because that refactor moves real execution paths, I smoke-tested both engines after it: queued gave 3 DEFERRED and 3 queue files, direct gave 1 STORED. Live-verified the guard on this machine, and ~/Gits still reports pending_count 122 — nothing marked.

On the deeper fix you raised

I still think the entry-point guard is right for this PR, but it only protects callers that go through the script: replay_entry called directly from library code still has no DB scoping. Closing that means a frontmatter schema change (replayed_into_db) plus is_pending_entry treating a non-canonical marker as still pending — not XS, and it would want its own live-check. Say the word and I will row it.

60 passed in tests/test_fallback_replay.py.


Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

EtanHey added a commit that referenced this pull request Sep 5, 2026
…d` (round 2)

Round-2 review, #774. All three real.

**@618-619 (medium) — round 1's own invariant, broken one line below where I stated
it.** `""` and `"   "` are PRESENT values. My round-1 docstring says only an ABSENT
key defaults to `stored`, and then the code did `if not text: return OUTCOME_STORED`.
`mcp/store_handler.py` takes a blank through `result.get("outcome", "stored")`
unchanged and `_store_receipt` rejects it, so this was fail-open on the strongest
claim in the vocabulary — the exact defect round 1 was supposed to close, one layer
down. Blank now goes to `unresolved` with the raw value in the error. Tested for
`""`, `"   "` and `"\t\n"`.

**@186-192 (low) — wrong word in the receipt.** A store answering
`{"outcome": "error"}` with no chunk id described its own failure; filing it as
`rejected` conflates two distinct #725 states. Now `error` when the store said so,
and still `rejected` when the store returned nothing at all and has no word for
itself. `_raw_store_outcome` extracted so the three call sites read the field one way.

**@1280-1295 (low) — the CLI contract was unpinned.** Unresolved was unit-tested but
nothing asserted the script exits 1 and persists `outcome_counts.unresolved` through
`--receipt`. A regression clearing the error on an unresolved row would have kept
exit 0 while still printing a soft label — a green run over a write nobody can vouch
for. Now pinned end to end.

Also moved `_guard_module` into this file as the shared CLI-test loader; #780 had
defined it locally and this is its right layer.

tests/test_fallback_replay.py 53 passed; 101 with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EtanHey added a commit that referenced this pull request Sep 5, 2026
…the guard (round 2)

Round-2 review, #780. Both real, measured on this APFS volume before the fix.

**@162 (HIGH) — a differently cased path to the real tree failed open.**

    os.path.samefile(~/Gits, ~/gits)  True
    Path.resolve() equal?             False      (~/Gits vs ~/gits)
    guard on ~/Gits                   REFUSED
    guard on ~/gits                   ALLOWED    <- the hole
    guard on ~/gits/brainlayer        ALLOWED    <- and its subtree

`resolve()` + `is_relative_to` is a string comparison. macOS is case-insensitive by
default and is the fleet's primary host; ubuntu CI is case-sensitive and would never
have surfaced this. Round 1 closed the subtree hole and left the casing one directly
beside it.

**One correction to the suggested remedy:** `realpath` + `os.path.normcase` does NOT
close this. `normcase` is a no-op on darwin — it only folds case on Windows — so on
the machine that has the bug it changes nothing. Only the inode knows, so both checks
now go through `os.path.samefile`, with a resolved-string compare only for the case
where neither path exists (two missing paths cannot be the same file).

`_names_real_gits_tree` walks to the nearest EXISTING ancestor before comparing,
because a subtree's leaf may not exist yet while the case alias is real one level up.
An `OSError` anywhere in it returns True: cannot prove it is outside the tree means
cannot prove it is safe.

**@165 (MEDIUM) — the allowlist refused a valid production drain.**

    samefile(brainlayer.db, Brainlayer.db)  True
    allowlisted?                            NO     <- refused an honest drain

Fail-closed is the safe direction, but it blocked the one shape this PR promises
keeps working. Now `_same_file` there too.

**@1474 (MEDIUM) — real-tree + canonical-DB never went through `main()`.** Every
allow-test called `marker_target_hazard` directly, so a regression in the wiring
rather than the predicate would have passed. That invocation is exactly what the
post-merge drain runs, and it is now driven end to end.

After, on the real machine — every shape, including the two that were wrong:

    ~/Gits · ~/gits · ~/GITS · ~/gits/brainlayer · ~/Gits/brainlayer/.worktrees/x   ALL REFUSED
    canonical DB + real tree           ALLOWED
    Brainlayer.db (case alias) + real  ALLOWED
    copy DB + a copy of the files      ALLOWED

`~/Gits` still reports pending_count 122 — nothing marked. Symlink cases were already
handled by `resolve()`; tests pin both, and the case tests skip with a stated reason
on case-sensitive filesystems so ubuntu CI says why rather than passing vacuously.

Rebuilt on #774's round-2 head rather than rebased: #774 now owns `_guard_module`,
which is the conflict. Same content otherwise, `C901` still clear on this file.

68 passed in tests/test_fallback_replay.py; 116 with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey
EtanHey force-pushed the wt/replay-d-marker-db-guard branch from 345d1ee to 5423289 Compare September 5, 2026 16:23
@cursor

cursor Bot commented Sep 5, 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_1e3c7836-1a82-40e9-acc9-bbff514608be)

@EtanHey

EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Round 2 — all three fixed. New head 54232898. Both findings measured before the fix.

@162 (HIGH) — a differently cased path to the real tree failed open

Reproduced on this APFS volume exactly as filed:

os.path.samefile(~/Gits, ~/gits)   True
Path.resolve() equal?              False      (~/Gits vs ~/gits)
guard on ~/Gits                    REFUSED
guard on ~/gits                    ALLOWED    ← the hole
guard on ~/gits/brainlayer         ALLOWED    ← and its subtree

Round 1 closed the subtree hole and left the casing one sitting directly beside it. Your read is right that ubuntu CI would never have surfaced it and that macOS is the host that matters here.

One correction to the suggested remedy: realpath + os.path.normcase does not close this. normcase is a no-op on darwin — it only folds case on Windows — so on the machine that has the bug it changes nothing. I went with os.path.samefile (inode) for both checks, keeping a resolved-string compare only for the case where neither path exists, since two missing paths cannot be the same file.

_names_real_gits_tree walks to the nearest existing ancestor before comparing, because a subtree's leaf may not exist yet while the case alias is real one level up. An OSError anywhere returns True: cannot prove it is outside the tree means cannot prove it is safe.

@165 (MEDIUM) — the allowlist refused a valid production drain

samefile(brainlayer.db, Brainlayer.db)   True
allowlisted?                             NO    ← refused an honest drain

Fail-closed is the safe direction, but it blocked the one shape this PR promises keeps working. _same_file there too.

@1474 (MEDIUM) — real-tree + canonical-DB never went through main()

Correct: every allow-test called marker_target_hazard directly, so a regression in the wiring rather than the predicate would have passed — and that invocation is exactly what the post-merge drain runs. Now driven end to end through main().

After, on the real machine — every shape, including the two that were wrong

~/Gits · ~/gits · ~/GITS · ~/gits/brainlayer · ~/Gits/brainlayer/.worktrees/x   ALL REFUSED

canonical DB + real tree            ALLOWED
Brainlayer.db (case alias) + real   ALLOWED
copy DB + a copy of the files       ALLOWED

~/Gits still reports pending_count 122 — nothing marked.

Symlink aliases were already handled by resolve(); there are now tests pinning both, and the case tests skip with a stated reason on case-sensitive filesystems so ubuntu CI says why rather than passing vacuously.

Note on the branch

Rebuilt on #774's round-2 head rather than rebased: #774 now owns _guard_module (its right layer) and that is what the rebase conflicted on. Same content otherwise, verified by test count and a clean run; C901 stays clear on this file.

68 passed in tests/test_fallback_replay.py; 116 with test_status_truthfulness / test_transport_replay_attribution / test_brainstore.


Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Comment thread tests/test_fallback_replay.py Outdated
entry = parse_fallback_file(_pending_file(repo, "docs.local/decisions/copy.md"))

class DummyStore:
def close(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

Comment thread tests/test_fallback_replay.py Outdated
entry = parse_fallback_file(_pending_file(repo, "docs.local/decisions/prod.md"))

class DummyStore:
def close(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

Comment thread tests/test_fallback_replay.py Outdated
entry = parse_fallback_file(_pending_file(repo, "docs.local/decisions/prod.md"))

class DummyStore:
def close(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

EtanHey added a commit that referenced this pull request Sep 5, 2026
…the guard (round 2)

Round-2 review, #780. Both real, measured on this APFS volume before the fix.

**@162 (HIGH) — a differently cased path to the real tree failed open.**

    os.path.samefile(~/Gits, ~/gits)  True
    Path.resolve() equal?             False      (~/Gits vs ~/gits)
    guard on ~/Gits                   REFUSED
    guard on ~/gits                   ALLOWED    <- the hole
    guard on ~/gits/brainlayer        ALLOWED    <- and its subtree

`resolve()` + `is_relative_to` is a string comparison. macOS is case-insensitive by
default and is the fleet's primary host; ubuntu CI is case-sensitive and would never
have surfaced this. Round 1 closed the subtree hole and left the casing one directly
beside it.

**One correction to the suggested remedy:** `realpath` + `os.path.normcase` does NOT
close this. `normcase` is a no-op on darwin — it only folds case on Windows — so on
the machine that has the bug it changes nothing. Only the inode knows, so both checks
now go through `os.path.samefile`, with a resolved-string compare only for the case
where neither path exists (two missing paths cannot be the same file).

`_names_real_gits_tree` walks to the nearest EXISTING ancestor before comparing,
because a subtree's leaf may not exist yet while the case alias is real one level up.
An `OSError` anywhere in it returns True: cannot prove it is outside the tree means
cannot prove it is safe.

**@165 (MEDIUM) — the allowlist refused a valid production drain.**

    samefile(brainlayer.db, Brainlayer.db)  True
    allowlisted?                            NO     <- refused an honest drain

Fail-closed is the safe direction, but it blocked the one shape this PR promises
keeps working. Now `_same_file` there too.

**@1474 (MEDIUM) — real-tree + canonical-DB never went through `main()`.** Every
allow-test called `marker_target_hazard` directly, so a regression in the wiring
rather than the predicate would have passed. That invocation is exactly what the
post-merge drain runs, and it is now driven end to end.

After, on the real machine — every shape, including the two that were wrong:

    ~/Gits · ~/gits · ~/GITS · ~/gits/brainlayer · ~/Gits/brainlayer/.worktrees/x   ALL REFUSED
    canonical DB + real tree           ALLOWED
    Brainlayer.db (case alias) + real  ALLOWED
    copy DB + a copy of the files      ALLOWED

`~/Gits` still reports pending_count 122 — nothing marked. Symlink cases were already
handled by `resolve()`; tests pin both, and the case tests skip with a stated reason
on case-sensitive filesystems so ubuntu CI says why rather than passing vacuously.

Rebuilt on #774's round-2 head rather than rebased: #774 now owns `_guard_module`,
which is the conflict. Same content otherwise, `C901` still clear on this file.

68 passed in tests/test_fallback_replay.py; 116 with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey
EtanHey force-pushed the wt/replay-d-marker-db-guard branch from 5423289 to 360a765 Compare September 5, 2026 16:37
@cursor

cursor Bot commented Sep 5, 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_4ce01801-2d75-49ea-ab17-1bfcc0c6e266)

@EtanHey

EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

DeepSource gate — real issues fixed (4 of the 5 introduced were mine). Head 360a765a.

Read with the authenticated deepsource CLI plus this run's "5 introduced" badge.

code where verdict
PTC-W0049 method doesn't use self DummyStore.close in the guard tests fixed — now @staticmethod
PY-R1000 cyclomatic complexity main() in scripts/replay_brain_store_fallbacks.py fixed — extracted _parse_legacy_entries; 10 → 7
(1 inherited from #774) reimport / no-self-use fixed there

On the complexity one: this PR does change main(), so I own it rather than calling it inherited. It is now 7, below the 10 it reached on this branch and below the 11 it has carried on origin/main since before this lane — so the file leaves this PR better than it arrived.

_parse_legacy_entries keeps the behaviour that matters: one unparseable legacy file still becomes its own ERROR receipt row instead of costing the whole batch.

Because that extraction moves a real execution path, I re-verified the guard on this machine rather than trusting the suite:

~/Gits · ~/gits · ~/gits/brainlayer     REFUSED
canonical DB + real tree                ALLOWED
Brainlayer.db (case alias) + real       ALLOWED
~/Gits pending_count                    122   (nothing marked)

68 passed. ruff check / ruff format --check clean.


Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Comment thread tests/test_fallback_replay.py Outdated

class DummyStore:
@staticmethod
def close():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

Comment thread tests/test_fallback_replay.py Outdated

class DummyStore:
@staticmethod
def close():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

Comment thread tests/test_fallback_replay.py Outdated

class DummyStore:
@staticmethod
def close():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

`close` doesn't contain any code


The function/method has been left empty here, without any comment or docstring. This can cause confusion later on why this was left empty.

except OSError:
# One of them does not exist, so they cannot be the same file. Compare resolved strings for
# the case where both are missing and merely spelled differently.
return os.path.realpath(left) == os.path.realpath(right)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High scripts/replay_brain_store_fallbacks.py:159

When the canonical DB file does not yet exist, _same_file returns False for case-only aliases such as Brainlayer.db and brainlayer.db, so the production replay is incorrectly refused on case-insensitive APFS. samefile cannot compare missing files and realpath preserves the differing final component; compare the existing parent inode and case-folded filename before falling back to exact paths.

-        return os.path.realpath(left) == os.path.realpath(right)
+        left = os.path.realpath(left)
+        right = os.path.realpath(right)
+        if sys.platform == "darwin":
+            try:
+                return (
+                    os.path.samefile(os.path.dirname(left), os.path.dirname(right))
+                    and os.path.basename(left).casefold() == os.path.basename(right).casefold()
+                )
+            except OSError:
+                pass
+        return left == right
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/replay_brain_store_fallbacks.py around line 159:

When the canonical DB file does not yet exist, `_same_file` returns `False` for case-only aliases such as `Brainlayer.db` and `brainlayer.db`, so the production replay is incorrectly refused on case-insensitive APFS. `samefile` cannot compare missing files and `realpath` preserves the differing final component; compare the existing parent inode and case-folded filename before falling back to exact paths.

EtanHey added a commit that referenced this pull request Sep 5, 2026
…the guard (round 2)

Round-2 review, #780. Both real, measured on this APFS volume before the fix.

**@162 (HIGH) — a differently cased path to the real tree failed open.**

    os.path.samefile(~/Gits, ~/gits)  True
    Path.resolve() equal?             False      (~/Gits vs ~/gits)
    guard on ~/Gits                   REFUSED
    guard on ~/gits                   ALLOWED    <- the hole
    guard on ~/gits/brainlayer        ALLOWED    <- and its subtree

`resolve()` + `is_relative_to` is a string comparison. macOS is case-insensitive by
default and is the fleet's primary host; ubuntu CI is case-sensitive and would never
have surfaced this. Round 1 closed the subtree hole and left the casing one directly
beside it.

**One correction to the suggested remedy:** `realpath` + `os.path.normcase` does NOT
close this. `normcase` is a no-op on darwin — it only folds case on Windows — so on
the machine that has the bug it changes nothing. Only the inode knows, so both checks
now go through `os.path.samefile`, with a resolved-string compare only for the case
where neither path exists (two missing paths cannot be the same file).

`_names_real_gits_tree` walks to the nearest EXISTING ancestor before comparing,
because a subtree's leaf may not exist yet while the case alias is real one level up.
An `OSError` anywhere in it returns True: cannot prove it is outside the tree means
cannot prove it is safe.

**@165 (MEDIUM) — the allowlist refused a valid production drain.**

    samefile(brainlayer.db, Brainlayer.db)  True
    allowlisted?                            NO     <- refused an honest drain

Fail-closed is the safe direction, but it blocked the one shape this PR promises
keeps working. Now `_same_file` there too.

**@1474 (MEDIUM) — real-tree + canonical-DB never went through `main()`.** Every
allow-test called `marker_target_hazard` directly, so a regression in the wiring
rather than the predicate would have passed. That invocation is exactly what the
post-merge drain runs, and it is now driven end to end.

After, on the real machine — every shape, including the two that were wrong:

    ~/Gits · ~/gits · ~/GITS · ~/gits/brainlayer · ~/Gits/brainlayer/.worktrees/x   ALL REFUSED
    canonical DB + real tree           ALLOWED
    Brainlayer.db (case alias) + real  ALLOWED
    copy DB + a copy of the files      ALLOWED

`~/Gits` still reports pending_count 122 — nothing marked. Symlink cases were already
handled by `resolve()`; tests pin both, and the case tests skip with a stated reason
on case-sensitive filesystems so ubuntu CI says why rather than passing vacuously.

Rebuilt on #774's round-2 head rather than rebased: #774 now owns `_guard_module`,
which is the conflict. Same content otherwise, `C901` still clear on this file.

68 passed in tests/test_fallback_replay.py; 116 with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey
EtanHey force-pushed the wt/replay-d-marker-db-guard branch from 360a765 to 436df81 Compare September 5, 2026 16:41
@cursor

cursor Bot commented Sep 5, 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_b2458beb-184b-45f8-b64e-27188cfede77)

allowed. Only the mismatch that hides data is refused.
"""
real_gits_root = (Path.home() / "Gits").expanduser()
if not _names_real_gits_tree(gits_root.expanduser(), real_gits_root):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High scripts/replay_brain_store_fallbacks.py:137

marker_target_hazard allows replay to proceed with a non-canonical DB when an outside --gits-root contains a symlink into ~/Gits; inventory_fallbacks then follows that symlink and writes replay markers into the real repository, hiding memories whose chunks exist only in the copy DB. Validate the repositories/paths that will be traversed (or reject symlinks resolving inside the real tree) before allowing replay.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/replay_brain_store_fallbacks.py around line 137:

`marker_target_hazard` allows replay to proceed with a non-canonical DB when an outside `--gits-root` contains a symlink into `~/Gits`; `inventory_fallbacks` then follows that symlink and writes replay markers into the real repository, hiding memories whose chunks exist only in the copy DB. Validate the repositories/paths that will be traversed (or reject symlinks resolving inside the real tree) before allowing replay.

EtanHey added a commit that referenced this pull request Sep 5, 2026
…the guard (round 2)

Round-2 review, #780. Both real, measured on this APFS volume before the fix.

**@162 (HIGH) — a differently cased path to the real tree failed open.**

    os.path.samefile(~/Gits, ~/gits)  True
    Path.resolve() equal?             False      (~/Gits vs ~/gits)
    guard on ~/Gits                   REFUSED
    guard on ~/gits                   ALLOWED    <- the hole
    guard on ~/gits/brainlayer        ALLOWED    <- and its subtree

`resolve()` + `is_relative_to` is a string comparison. macOS is case-insensitive by
default and is the fleet's primary host; ubuntu CI is case-sensitive and would never
have surfaced this. Round 1 closed the subtree hole and left the casing one directly
beside it.

**One correction to the suggested remedy:** `realpath` + `os.path.normcase` does NOT
close this. `normcase` is a no-op on darwin — it only folds case on Windows — so on
the machine that has the bug it changes nothing. Only the inode knows, so both checks
now go through `os.path.samefile`, with a resolved-string compare only for the case
where neither path exists (two missing paths cannot be the same file).

`_names_real_gits_tree` walks to the nearest EXISTING ancestor before comparing,
because a subtree's leaf may not exist yet while the case alias is real one level up.
An `OSError` anywhere in it returns True: cannot prove it is outside the tree means
cannot prove it is safe.

**@165 (MEDIUM) — the allowlist refused a valid production drain.**

    samefile(brainlayer.db, Brainlayer.db)  True
    allowlisted?                            NO     <- refused an honest drain

Fail-closed is the safe direction, but it blocked the one shape this PR promises
keeps working. Now `_same_file` there too.

**@1474 (MEDIUM) — real-tree + canonical-DB never went through `main()`.** Every
allow-test called `marker_target_hazard` directly, so a regression in the wiring
rather than the predicate would have passed. That invocation is exactly what the
post-merge drain runs, and it is now driven end to end.

After, on the real machine — every shape, including the two that were wrong:

    ~/Gits · ~/gits · ~/GITS · ~/gits/brainlayer · ~/Gits/brainlayer/.worktrees/x   ALL REFUSED
    canonical DB + real tree           ALLOWED
    Brainlayer.db (case alias) + real  ALLOWED
    copy DB + a copy of the files      ALLOWED

`~/Gits` still reports pending_count 122 — nothing marked. Symlink cases were already
handled by `resolve()`; tests pin both, and the case tests skip with a stated reason
on case-sensitive filesystems so ubuntu CI says why rather than passing vacuously.

Rebuilt on #774's round-2 head rather than rebased: #774 now owns `_guard_module`,
which is the conflict. Same content otherwise, `C901` still clear on this file.

68 passed in tests/test_fallback_replay.py; 116 with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey
EtanHey force-pushed the wt/replay-d-marker-db-guard branch from 436df81 to 5f4c69e Compare September 5, 2026 16:44
@cursor

cursor Bot commented Sep 5, 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_d545fab1-3068-42cd-a9b7-1b0f03988578)

case alias shows up at whatever level is real.
"""
try:
if _same_file(target, real_gits_root):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High scripts/replay_brain_store_fallbacks.py:174

_names_real_gits_tree rejects a valid copied-tree replay when --gits-root contains .., such as ~/Gits/../gits-copy. Because Path.parents is traversed before normalization, it encounters the lexical ~/Gits ancestor and incorrectly refuses the run; normalize target before checking its ancestors.

-        if _same_file(target, real_gits_root):
+        target = target.resolve()
+        if _same_file(target, real_gits_root):
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/replay_brain_store_fallbacks.py around line 174:

`_names_real_gits_tree` rejects a valid copied-tree replay when `--gits-root` contains `..`, such as `~/Gits/../gits-copy`. Because `Path.parents` is traversed before normalization, it encounters the lexical `~/Gits` ancestor and incorrectly refuses the run; normalize `target` before checking its ancestors.

@EtanHey

EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

DeepSource gate — now GREEN. Head 5f4c69e5.

DeepSource: Python = success on this head.

code where verdict
PTC-W0049 function/method with an empty body DummyStore.close stubs in the guard tests fixed — now reuse ClosableStoreStub from the base
PY-R1000 cyclomatic complexity main() in scripts/replay_brain_store_fallbacks.py fixed — _parse_legacy_entries extracted, 10 → 7

Correcting my first gate pass here too: I read PTC-W0049 as no-self-use and applied @staticmethod, which left the pass in place and changed nothing. The rule is empty body.

On complexity: this PR does change main(), so I own it. It is now 7 — below the 10 it reached here and below the 11 origin/main has carried since before this lane, so the file leaves better than it arrived. _parse_legacy_entries keeps the behaviour that matters: one unparseable legacy file still becomes its own ERROR receipt row instead of costing the batch.

Because that extraction moves a real execution path, I re-verified on this machine rather than trusting the suite:

~/Gits · ~/gits · ~/gits/brainlayer     REFUSED
canonical DB + real tree                ALLOWED
Brainlayer.db (case alias) + real       ALLOWED
~/Gits pending_count                    122   (nothing marked)

68 passed. ruff check / ruff format --check clean.


Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

EtanHey added a commit that referenced this pull request Sep 5, 2026
…t that it ran (a, 1/3) (XS) (#774)

* feat(fallback-replay): the receipt says what each replay did, not just that it ran (a, 1/3)

`ReplayResult` carried a chunk id and nothing about the write behind it, so a drain
of 122 pending fallback files printed 122 identical-looking REPLAYED lines whether
every memory was freshly inserted, folded into an existing chunk, or a bare
re-send that wrote nothing. `store_memory` has reported `outcome` since #725; the
replay path threw it away.

- `ReplayResult.outcome` over the #725 vocabulary — stored / duplicate / merged /
  deferred / rejected / error — plus `skipped` for a file that was already stored.
  Defaults to `skipped`, so a result built without one never claims a write.
- `replay_entry` reads the store's outcome (defaulting to `stored` the way
  `mcp/store_handler.py` does), `rejected` when no chunk id came back, `error`
  when the store raised.
- `queue_entry` answers `deferred`: the row is durably queued and the drain
  commits it later. Per #725 that is success, not a failure to re-drive.
- `scripts/replay_brain_store_fallbacks.py` prints the outcome per file, totals
  them as `outcome_counts`, and `--receipt PATH` persists the JSON receipt.

Live-check against a COPY of the real DB (ratified 2026-08-02 — this touches
stored data), using the sanctioned daily backup `backups/2026-09-05.db`
APFS-cloned, and a copy of the real fallback files so production markers stayed
untouched. Reproduced the debt exactly (pending_count 122), then:

  run 1: outcomes stored=122, errors=0, 122/122 chunk ids present in the copy,
         pending_count 0, green true
  run 2 (same copy, fresh file copy): outcomes duplicate=122, same chunk ids

Before this change both runs emit the same text. That is the whole defect.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(fallback-replay): an unrecognized store outcome is never upgraded to `stored` (round 1)

Round-1 review, #774 @605 and @221.

**@605, real — and my PR body cited the wrong line.** I claimed parity with
`mcp/store_handler.py:1126`. That line defaults only when the key is ABSENT
(`result.get("outcome", "stored")`), and `_store_receipt` then *raises* on a present
value outside the vocabulary and again on one that is present but unresolved
(`store_handler.py:601-606`). My `_store_outcome` remapped ANY present unrecognized
value — `deferred`, `rejected`, `error`, garbage — to `stored`, the strongest claim
in the vocabulary. That is weaker than both halves of the contract, and the exact
inverse of what a receipt is for.

Now: absent still defaults to `stored` (a store predating the field did commit a
write, and that parity must not regress). A present non-resolved value gets its own
`OUTCOME_UNRESOLVED` and an error naming the raw word, so the row is traceable and
the script exits non-zero instead of reporting a clean run. Not a raise: raising
would abandon the rest of a 122-file batch for one odd row, which is why the MCP
path's exact behaviour is not the right one here.

**@221, a coverage gap rather than a defect.** The already-queued -> `deferred`
early return was already correct; three new tests pin it, including the negative
side (a vanished queue file must re-enqueue and stay `deferred`) — the case where a
regression labelling it `skipped` would report queue debt as already handled.

Tests written first: the two `_store_outcome` tests failed, the three `queue_entry`
tests passed on the unfixed code, which is what identified @221 as coverage.

Verified against round-1's DeepSource note: the six findings under
`ruff --select TRY,PLW,B,S,SIM,C90` are byte-identical between `origin/main` and this
branch — including `C901 main is too complex (11 > 10)`, which is at
`replay_script.py:31` on main already. This diff introduces none of them.

tests/test_fallback_replay.py 50 passed; with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore, 98 passed.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(fallback-replay): a blank outcome is present, so it is not `stored` (round 2)

Round-2 review, #774. All three real.

**@618-619 (medium) — round 1's own invariant, broken one line below where I stated
it.** `""` and `"   "` are PRESENT values. My round-1 docstring says only an ABSENT
key defaults to `stored`, and then the code did `if not text: return OUTCOME_STORED`.
`mcp/store_handler.py` takes a blank through `result.get("outcome", "stored")`
unchanged and `_store_receipt` rejects it, so this was fail-open on the strongest
claim in the vocabulary — the exact defect round 1 was supposed to close, one layer
down. Blank now goes to `unresolved` with the raw value in the error. Tested for
`""`, `"   "` and `"\t\n"`.

**@186-192 (low) — wrong word in the receipt.** A store answering
`{"outcome": "error"}` with no chunk id described its own failure; filing it as
`rejected` conflates two distinct #725 states. Now `error` when the store said so,
and still `rejected` when the store returned nothing at all and has no word for
itself. `_raw_store_outcome` extracted so the three call sites read the field one way.

**@1280-1295 (low) — the CLI contract was unpinned.** Unresolved was unit-tested but
nothing asserted the script exits 1 and persists `outcome_counts.unresolved` through
`--receipt`. A regression clearing the error on an unresolved row would have kept
exit 0 while still printing a soft label — a green run over a write nobody can vouch
for. Now pinned end to end.

Also moved `_guard_module` into this file as the shared CLI-test loader; #780 had
defined it locally and this is its right layer.

tests/test_fallback_replay.py 53 passed; 101 with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tests): clear the two DeepSource issues this PR actually introduced (round 2 gate)

Read the real occurrences with `deepsource issues list --repo gh/EtanHey/brainlayer
--analyzer python --json` (CLI, authenticated) plus each run's introduced count,
instead of inferring from the JS dashboard as I did in round 1. **My earlier "not
introduced by these PRs" claim was wrong** — this run reports 3 introduced, and two
of them are mine.

**PYL-W0404 (reimported)** — `tests/test_fallback_replay.py:645`, `import json`
inside `jsonl_enqueue_func`. That inner import predates this lane; it only BECAME a
reimport when I added `import json` at module level for the `--receipt` test. My diff
caused it, so my diff removes it.

**PTC-W0049 (method doesn't use `self`)** — the `DummyStore.close` stubs my tests
added. Now `@staticmethod`; they are called as `store.close()` either way.

53 passed in tests/test_fallback_replay.py.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tests): a stub with a `pass` body IS the PTC-W0049 defect (round 2 gate, second pass)

My first pass at this misread the rule. I assumed PTC-W0049 was "method doesn't use
`self`" from the code snippet DeepSource renders and reached for `@staticmethod`. The
run came back with the same 3 introduced. Looked up the actual title instead of
guessing again:

    PTC-W0049 = "Function/method with an empty body"

So the `pass` was the defect the whole time and `@staticmethod` never touched it.

The three nested `class DummyStore: def close(): pass` stubs are replaced by one
module-level `ClosableStoreStub` that records `self.closed = True`. Non-empty body,
uses `self`, no duplication across three call sites — and a double that remembers
being closed is more useful than one that silently does nothing.

Verified with an AST scan over this branch's added lines: no empty-body function
remains in anything this diff introduced.

53 passed. `ruff check` / `ruff format --check` clean.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: remove a scratch analysis script I committed by accident

`.empty_body_check.py` was a throwaway AST scan I wrote to audit my own diff for
empty-body functions. A `git add -A` swept it into the PR, and DeepSource flagged it
(`PYL-W1510`, `subprocess.run` without `check`) — correctly, since it was never meant
to ship. Removed.

My mistake, not the analyzer's; the finding is real and the file has no business in
this repo.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey
EtanHey changed the base branch from wt/replay-a-receipt-outcomes to main September 5, 2026 17:13
EtanHey and others added 4 commits September 5, 2026 20:14
… is not canonical (d)

Rebuilt on #774's round-2 head. Same content as 345d1ee; the duplicate
`_guard_module` loader is dropped because #774 now owns it (its right layer), which
is what the rebase conflicted on.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the guard (round 2)

Round-2 review, #780. Both real, measured on this APFS volume before the fix.

**@162 (HIGH) — a differently cased path to the real tree failed open.**

    os.path.samefile(~/Gits, ~/gits)  True
    Path.resolve() equal?             False      (~/Gits vs ~/gits)
    guard on ~/Gits                   REFUSED
    guard on ~/gits                   ALLOWED    <- the hole
    guard on ~/gits/brainlayer        ALLOWED    <- and its subtree

`resolve()` + `is_relative_to` is a string comparison. macOS is case-insensitive by
default and is the fleet's primary host; ubuntu CI is case-sensitive and would never
have surfaced this. Round 1 closed the subtree hole and left the casing one directly
beside it.

**One correction to the suggested remedy:** `realpath` + `os.path.normcase` does NOT
close this. `normcase` is a no-op on darwin — it only folds case on Windows — so on
the machine that has the bug it changes nothing. Only the inode knows, so both checks
now go through `os.path.samefile`, with a resolved-string compare only for the case
where neither path exists (two missing paths cannot be the same file).

`_names_real_gits_tree` walks to the nearest EXISTING ancestor before comparing,
because a subtree's leaf may not exist yet while the case alias is real one level up.
An `OSError` anywhere in it returns True: cannot prove it is outside the tree means
cannot prove it is safe.

**@165 (MEDIUM) — the allowlist refused a valid production drain.**

    samefile(brainlayer.db, Brainlayer.db)  True
    allowlisted?                            NO     <- refused an honest drain

Fail-closed is the safe direction, but it blocked the one shape this PR promises
keeps working. Now `_same_file` there too.

**@1474 (MEDIUM) — real-tree + canonical-DB never went through `main()`.** Every
allow-test called `marker_target_hazard` directly, so a regression in the wiring
rather than the predicate would have passed. That invocation is exactly what the
post-merge drain runs, and it is now driven end to end.

After, on the real machine — every shape, including the two that were wrong:

    ~/Gits · ~/gits · ~/GITS · ~/gits/brainlayer · ~/Gits/brainlayer/.worktrees/x   ALL REFUSED
    canonical DB + real tree           ALLOWED
    Brainlayer.db (case alias) + real  ALLOWED
    copy DB + a copy of the files      ALLOWED

`~/Gits` still reports pending_count 122 — nothing marked. Symlink cases were already
handled by `resolve()`; tests pin both, and the case tests skip with a stated reason
on case-sensitive filesystems so ubuntu CI says why rather than passing vacuously.

Rebuilt on #774's round-2 head rather than rebased: #774 now owns `_guard_module`,
which is the conflict. Same content otherwise, `C901` still clear on this file.

68 passed in tests/test_fallback_replay.py; 116 with test_status_truthfulness,
test_transport_replay_attribution and test_brainstore.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urther (round 2 gate)

Read with the authenticated `deepsource` CLI plus this run's introduced count: **5
introduced**, and the ones attributable to my diff are cleared here.

**PTC-W0049 (method doesn't use `self`)** — three more `DummyStore.close` stubs in
the guard tests. Now `@staticmethod`.

**PY-R1000 (cyclomatic complexity)** — reported against `main()`, which this PR does
change, so I own it rather than call it inherited. `_parse_legacy_entries` extracted
(one unparseable legacy file still becomes its own ERROR receipt row instead of
losing the batch). `main()` is now **7**, down from 10 on this branch and from the
**11 it has carried on `origin/main`** since before this lane.

Re-verified after the extraction, since it moves a real execution path:

    ~/Gits · ~/gits · ~/gits/brainlayer   REFUSED
    canonical DB + real tree              ALLOWED
    Brainlayer.db (case alias) + real     ALLOWED

`~/Gits` still reports pending_count 122. 68 passed in tests/test_fallback_replay.py.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…second pass)

PTC-W0049 is "Function/method with an empty body" — I had misread it as no-self-use
and reached for `@staticmethod`, which left the `pass` untouched. The three guard-test
stubs here now reuse `ClosableStoreStub` from #774's base instead of redefining an
empty class each time.

AST scan over this branch's added lines: no empty-body function remains in anything
this diff introduces.

68 passed. `ruff check` / `ruff format --check` clean.

Agent: brainlayerClaude-ca745a2b (claude-opus-5[1m])

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rendered = json.dumps(result, indent=2, sort_keys=True)
if receipt_path is not None:
target = receipt_path.expanduser()
target.parent.mkdir(parents=True, exist_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium scripts/replay_brain_store_fallbacks.py:320

A receipt failure at target.parent.mkdir(...) or target.write_text(...) aborts _emit with an uncaught filesystem exception, so a completed replay produces neither its normal stdout result nor a usable receipt. Catch the receipt write error and route it through the CLI's controlled error reporting and exit path.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/replay_brain_store_fallbacks.py around line 320:

A receipt failure at `target.parent.mkdir(...)` or `target.write_text(...)` aborts `_emit` with an uncaught filesystem exception, so a completed replay produces neither its normal stdout result nor a usable receipt. Catch the receipt write error and route it through the CLI's controlled error reporting and exit path.

@EtanHey
EtanHey force-pushed the wt/replay-d-marker-db-guard branch from 5f4c69e to 9d36a60 Compare September 5, 2026 17:15
@cursor

cursor Bot commented Sep 5, 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_a7ee0c0c-4f2e-4e78-881e-b135577498c5)

@github-actions

github-actions Bot commented Sep 5, 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 9d36a6097d5c == PR head · checkout d1afd1cf56c2 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 33980263749 · main 24482318d82d · 2026-09-05T17:13:16Z) 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; today no runner-side collector measures any baseline field, so today the baseline cannot move by PR at all, and this row says so instead of a hand edit passing. Boundary: the comparator is this PR's checkout of ci_ratchet_table.py, diff-reviewable, not tamper-proof.
provenance 🟢 GREEN stamped d1afd1cf56c2 == 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 9d36a6097d5c · PR head 9d36a6097d5c · checkout d1afd1cf56c2 · run · updated 2026-09-05 17:16:10 UTC

if ancestor.exists() and _same_file(ancestor, real_gits_root):
return True
return target.resolve().is_relative_to(real_gits_root.resolve())
except OSError:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High scripts/replay_brain_store_fallbacks.py:180

--apply --gits-root pointing to a symlink loop crashes with RuntimeError instead of returning the documented fail-closed refusal. Path.resolve(strict=False) raises RuntimeError for symlink loops on Python 3.11/3.12, but this preflight only catches OSError; catch RuntimeError as well.

Suggested change
except OSError:
except (OSError, RuntimeError):
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/replay_brain_store_fallbacks.py around line 180:

`--apply --gits-root` pointing to a symlink loop crashes with `RuntimeError` instead of returning the documented fail-closed refusal. `Path.resolve(strict=False)` raises `RuntimeError` for symlink loops on Python 3.11/3.12, but this preflight only catches `OSError`; catch `RuntimeError` as well.

@EtanHey
EtanHey merged commit 02ebdae into main Sep 5, 2026
18 checks passed
@EtanHey
EtanHey deleted the wt/replay-d-marker-db-guard branch September 5, 2026 17:33
EtanHey added a commit that referenced this pull request Sep 5, 2026
All six sites: pyproject.toml, src/brainlayer/__init__.py, server.json
(root + packages[0]), brain-bar/bundle/Info.plist (short, bundle, release).
Casks/brainbar.rb stays 1.5.9: no Swift change this release, declared to
scripts/brainlayer-version-check.sh via
BRAINLAYER_VERSION_CHECK_CASK_LAG_REASON="no BrainBar release for 1.5.16"
(kept in the PR body, never exported into the push env — two tests assert
the check fails without a reason).

Why a release, 22 minutes after the last one. v1.5.15 was tagged at
51a72a0 (20:12:29). #778 — the index runtime watchdog — merged at
78d92bc (20:34:55), so it is NOT in 1.5.15. Verified:
`git merge-base --is-ancestor 78d92bc v1.5.15^{}` -> false, and the
installed keg's python raises
`ModuleNotFoundError: No module named 'brainlayer.index_watchdog'`.
That watchdog is the fix for the M1's nightly `brainlayer index` job,
which ran 14h03m at ~100% CPU on 09-05, 10h past its own 4h cap. Both
Macs' 03:15 index jobs are `launchctl disable`d as a stopgap and come
back ON only after 1.5.16 is installed and the watchdog is proven
present.

Nine commits ride along: #774 #777 #780 #778 #783 #785 #786 #787 #788.

Co-authored-by: brainlayerClaude-c1601b03 running claude-opus-5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

XS Extra-small change (400 lines or fewer)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant