Skip to content

perf(replay): N+1 belief loop → set-based query in _compute_replay_drift_report (#725) - #726

Merged
github-actions[bot] merged 5 commits into
mainfrom
perf/issue-725-replay-canonical-orphans-set-query
May 13, 2026
Merged

perf(replay): N+1 belief loop → set-based query in _compute_replay_drift_report (#725)#726
github-actions[bot] merged 5 commits into
mainfrom
perf/issue-725-replay-canonical-orphans-set-query

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented May 12, 2026

Copy link
Copy Markdown
Owner

Closes #725.

Summary

Replaces the N+1 belief-iteration loop in src/aelfrice/replay.py:_compute_replay_drift_report with a single set-based store query. Adds MemoryStore.list_canonical_orphans(limit: int | None) -> list[tuple[str, str | None]] to encapsulate the SQL, matching the rest of store.py's pattern (the existing synthesize_legacy_log path already uses the same json_each(derived_belief_ids) + NOT IN shape).

Diff

Four atomic SSH-signed commits:

  1. feat(store): list_canonical_orphans set-based query (#725) — new method in src/aelfrice/store.py. Single SQL pass: build the set of belief ids that appear in derived_belief_ids of at least one non-legacy_unknown log row, then NOT IN selects the complement. ORDER BY b.id ASC for sample stability (matches list_belief_ids()).
  2. test(store): unit tests for list_canonical_orphans (#725) — 9 new tests in tests/test_store_crud.py covering empty store, all-legacy beliefs, mixed, with/without limit, ordering, and pre-[v2.0] Write log as source of truth — append-only ingest_log + replay-capable derivation #205 store with no log rows.
  3. perf(replay): use list_canonical_orphans in _compute_replay_drift_report (#725) — replaces the N+1 loop. Removes the TODO(perf): comment. The function output (canonical_orphan count + examples_canonical_orphan sample) is byte-identical.
  4. docs(changelog): #725 N+1 → set-based query in replay drift report — entry under ### Performance in ## [3.0.0] - Unreleased.

Touched files: src/aelfrice/store.py (+43), src/aelfrice/replay.py (+7/-19), tests/test_store_crud.py (+128), CHANGELOG.md (+2).

Acceptance (from #725)

  • MemoryStore.list_canonical_orphans() method with unit tests in the tests/test_store_crud.py pattern.
  • replay.py:339-360 consumes the new method; N+1 loop and TODO(perf): comment removed.
  • tests/test_replay.py::test_compute_replay_drift_report_* pass byte-identically (all 20 test_replay_full_equality tests + 22 test_store_crud tests green; full suite 3764 passed, 59 skipped, 75 xfailed).
  • Bench evidence (below).

Bench

Synthetic store, 50% canonical-orphans, wall-clock for the canonical-orphan section only (averaged across 3 runs):

N orphans old (N+1 loop) new (set-based) speedup
1,000 500 2,534 ms 0.7 ms ~3,500×
5,000 2,500 65,906 ms 3.8 ms ~17,000×

The old shape is O(N²) due to a full ingest_log table scan per belief (iter_ingest_log_for_belief is documented as "Linear scan — v2.0 first slice has no inverted index"). New shape is O(N) — one set-build pass.

The public v0.1 soak corpus has canonical_orphan == 0 (all beliefs have non-legacy log rows), so run_replay_soak wall-clock (~9 ms) is noise-dominated before/after; the gain only surfaces at operator-store scale.

Verification

  • git log --format='%G?' github/main..HEAD — all four commits signed (G).
  • Discretion grep on the diff: empty.
  • Branch is FF on github/main.
  • uv run pytest -x -q: 3764 passed, 59 skipped, 75 xfailed.
  • iter_ingest_log_for_belief untouched (other callers exist; out of scope per perf(replay): replace N+1 belief loop in _compute_replay_drift_report with set-based query #725).
  • Sample-stability check: list_belief_ids() uses ORDER BY id ASC (store.py:1474); new list_canonical_orphans() uses ORDER BY b.id ASC. Order preserved.

Out of scope (per #725)

  • iter_ingest_log_for_belief method — kept; other callers exist.
  • Broader replay performance pass — separate issue if surfaced.

Summary by Sourcery

Replace the canonical-orphan computation in the replay drift report with a set-based store query for better performance while preserving output.

New Features:

  • Add MemoryStore.list_canonical_orphans() API to retrieve canonical orphan beliefs and their content hashes.

Enhancements:

  • Refactor replay drift report to use the new list_canonical_orphans() helper instead of an N+1 per-belief scan, significantly improving performance on large stores.

Documentation:

  • Document the replay drift report performance improvement in the unreleased 3.0.0 changelog.

Tests:

  • Add unit tests for list_canonical_orphans() covering empty, legacy-only, mixed, limited, and ordered orphan sets.

Summary by CodeRabbit

  • Chores

    • Deterministic listing of canonical orphan records with optional limit, stable ordering, and clearer handling of missing content hashes for more reliable reporting and maintenance.
  • Refactor

    • Simplified orphan-detection logic to improve performance and clarity of results.
  • Tests

    • Added comprehensive tests validating orphan selection, ordering, limit behavior, and returned record details to ensure correctness and prevent regressions.

Review Change Stack

@sourcery-ai

sourcery-ai Bot commented May 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Optimizes canonical-orphan detection in replay drift reports by introducing a set-based store query and wiring it into replay, with targeted tests and changelog documentation.

Sequence diagram for canonical orphan computation in replay drift report

sequenceDiagram
    participant ReplayModule as replay_full_equality
    participant MemoryStore
    participant SQLite

    ReplayModule->>MemoryStore: list_canonical_orphans()
    MemoryStore->>SQLite: execute(SELECT b.id, b.content_hash ...)
    SQLite-->>MemoryStore: rows(belief_id, content_hash)
    MemoryStore-->>ReplayModule: all_orphans

    ReplayModule->>ReplayModule: canonical_orphan = len(all_orphans)
    loop for bid, content_hash in all_orphans[:drift_examples]
        ReplayModule->>ReplayModule: append to examples_canonical_orphan
    end
Loading

File-Level Changes

Change Details Files
Add MemoryStore.list_canonical_orphans to perform set-based canonical-orphan detection via a single SQL query.
  • Introduce list_canonical_orphans(limit) returning (belief_id, content_hash) tuples for canonical orphan beliefs.
  • Implement a NOT IN subquery over ingest_log/json_each(derived_belief_ids) to exclude beliefs with any non-legacy log rows, treating remaining beliefs as orphans.
  • Order results by belief id ascending and support optional LIMIT to cap returned orphans while preserving sample stability.
  • Map SQLite row values to Python tuple types, preserving None for missing content_hash.
src/aelfrice/store.py
Add unit tests for list_canonical_orphans capturing legacy vs non-legacy ingest-log combinations and ordering/limit behavior.
  • Import ingest source constants needed to construct legacy and non-legacy log rows in tests.
  • Cover empty-store, no-log-rows, all-legacy-rows, and non-legacy-row-excludes-befief cases.
  • Cover mixed-belief and mixed-log-rows-for-same-belief scenarios to validate classification semantics.
  • Test limit parameter behavior, ordering by id ASC, and correctness of returned content_hash values.
tests/test_store_crud.py
Refactor replay drift report canonical-orphan computation to use list_canonical_orphans instead of an N+1 belief loop.
  • Replace per-belief iteration over ingest_log with a single call to store.list_canonical_orphans().
  • Compute canonical_orphan count as the length of the orphan list instead of incrementing in a loop.
  • Build examples_canonical_orphan directly from the orphan tuples, slicing by drift_examples for sampling.
  • Remove the obsolete TODO(perf) comment about replacing the N+1 iteration.
src/aelfrice/replay.py
Document the canonical-orphan performance optimization in the changelog. CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#725 Implement a set-based `MemoryStore.list_canonical_orphans(limit: int None) -> list[tuple[str, str None]]method encapsulating the SQL that identifies canonical-orphan beliefs, preserving deterministicORDER BY id ASC` ordering and supporting an optional limit, and add unit tests.
#725 Refactor _compute_replay_drift_report in replay.py to replace the N+1 per-belief loop with the new list_canonical_orphans method, remove the TODO(perf) comment, and keep output (counts and examples) byte-identical, including sample ordering.
#725 Benchmark the new implementation versus the old N+1 loop on relevant scales and document the wall-time delta in the PR (and associated documentation such as CHANGELOG), ensuring tests still pass.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label May 12, 2026
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3ed832f9-87d7-40a4-83d5-ac7593dc50ba

📥 Commits

Reviewing files that changed from the base of the PR and between a7fa14f and a8d0b8d.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/CHANGELOG.md
📒 Files selected for processing (3)
  • src/aelfrice/replay.py
  • src/aelfrice/store.py
  • tests/test_store_crud.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/aelfrice/store.py
  • tests/test_store_crud.py

📝 Walkthrough

Walkthrough

Replaces per-belief ingest-log scans in replay drift reporting with a set-based store query. Adds MemoryStore.list_canonical_orphans(limit) and updates replay.py to use it; tests verify selection logic, ordering, limiting, and content_hash return values.

Changes

Canonical orphan query optimization

Layer / File(s) Summary
Canonical orphan store method and tests
src/aelfrice/store.py, tests/test_store_crud.py
Adds MemoryStore.list_canonical_orphans(limit=None) that uses a set-based SQL query excluding beliefs referenced by any non-legacy_unknown ingest-log rows; tests cover empty-store, legacy vs non-legacy rows, mixed cases, limiting, id-ascending ordering, and content_hash propagation.
Replay refactoring to use canonical orphan query
src/aelfrice/replay.py
Replaces the per-belief N+1 ingest-log scanning loop in replay_full_equality() with a call to store.list_canonical_orphans(), using its length for counts and returned tuples for drift examples.

Sequence Diagram(s)

sequenceDiagram
  participant Replay as replay._compute_replay_drift_report
  participant Store as MemoryStore.list_canonical_orphans
  participant DB as SQLite

  Replay->>Store: call list_canonical_orphans(limit)
  Store->>DB: SELECT b.id, b.content_hash WHERE b.id NOT IN (SELECT belief_id FROM ingest_log WHERE source_kind != 'legacy_unknown')
  DB-->>Store: rows (id, content_hash)
  Store-->>Replay: list[(belief_id, content_hash)...]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

attn:review, author-Setr

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf(replay): N+1 belief loop → set-based query in _compute_replay_drift_report (#725)' clearly and concisely describes the main change: replacing an N+1 loop with a set-based query optimization.
Description check ✅ Passed The PR description comprehensively covers all required sections including summary, linked issues, type of change (perf), verification steps, test plan, detailed diff structure, acceptance criteria, and benchmark results.
Linked Issues check ✅ Passed All acceptance criteria from issue #725 are met: the new MemoryStore.list_canonical_orphans() method is implemented with unit tests, replay.py now uses it instead of the N+1 loop with the TODO comment removed, replay tests pass byte-identically, and bench evidence shows significant speedup.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to issue #725 objectives: new method in store.py, consuming refactor in replay.py, tests in test_store_crud.py, and changelog entry. Out-of-scope items (iter_ingest_log_for_belief and broader replay perf work) are explicitly excluded as stated.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/issue-725-replay-canonical-orphans-set-query

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 and usage tips.

@robotrocketscience robotrocketscience added the author-schwartzchild Authored by parallel session schwartzchild label May 12, 2026
@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 200 changed lines (limit: 200)
  • 4 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 security issue, and left some high level feedback:

Security issues:

  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)

General comments:

  • The new list_canonical_orphans query changes the handling of NULL source_kind rows vs. the previous Python logic (str(r.get('source_kind', '')) != INGEST_SOURCE_LEGACY_UNKNOWN treats missing/None as non-legacy), so if source_kind can be NULL in practice you may want to explicitly COALESCE(il.source_kind, '') != ? (or similar) to preserve the old behavior.
  • To avoid subtle issues with NOT IN if json_each ever yielded NULL values (or future schema changes allow them), consider switching the subquery to a SELECT DISTINCT + WHERE je.value IS NOT NULL or using a NOT EXISTS pattern instead.
  • The limit_clause is interpolated directly into the SQL string; even though limit is an internal int, you could use a parameterized LIMIT ? for consistency with the rest of the store queries and to avoid hand-crafted SQL fragments.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `list_canonical_orphans` query changes the handling of `NULL` `source_kind` rows vs. the previous Python logic (`str(r.get('source_kind', '')) != INGEST_SOURCE_LEGACY_UNKNOWN` treats missing/None as non-legacy), so if `source_kind` can be `NULL` in practice you may want to explicitly `COALESCE(il.source_kind, '') != ?` (or similar) to preserve the old behavior.
- To avoid subtle issues with `NOT IN` if `json_each` ever yielded `NULL` values (or future schema changes allow them), consider switching the subquery to a `SELECT DISTINCT` + `WHERE je.value IS NOT NULL` or using a `NOT EXISTS` pattern instead.
- The `limit_clause` is interpolated directly into the SQL string; even though `limit` is an internal int, you could use a parameterized `LIMIT ?` for consistency with the rest of the store queries and to avoid hand-crafted SQL fragments.

## Individual Comments

### Comment 1
<location path="src/aelfrice/store.py" line_range="1500-1513" />
<code_context>
        cur = self._conn.execute(
            f"""
            SELECT b.id, b.content_hash
            FROM beliefs b
            WHERE b.id NOT IN (
                SELECT je.value
                FROM ingest_log il, json_each(il.derived_belief_ids) je
                WHERE il.derived_belief_ids IS NOT NULL
                  AND il.source_kind != ?
            )
            ORDER BY b.id ASC{limit_clause}
            """,
            (INGEST_SOURCE_LEGACY_UNKNOWN,),
        )
</code_context>
<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/aelfrice/store.py Outdated

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/aelfrice/store.py (1)

1499-1517: ⚡ Quick win

This is a read-only query not covered by the file's write-path guideline.

The list_canonical_orphans() method performs only a SELECT query and is not part of the belief write path, so the file's coding guideline (which applies to SQL bypassing broker-confidence attenuation in WRITE operations) does not apply here. While the f-string LIMIT clause creates a style risk that static analysis flags, the int(limit) conversion provides runtime safety.

If you want to refactor this for consistency with general SQL best practices, Option 2 (sentinel value) is simpler than Option 1 (conditional queries).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aelfrice/store.py` around lines 1499 - 1517, The query in
list_canonical_orphans() is read-only and the file-level write-path guideline
doesn't apply; to remove the f-string style risk while keeping runtime safety,
replace the dynamic f-string LIMIT construction (limit_clause) with a
parameterized LIMIT: build the SQL without interpolated strings and, if limit is
provided, append " LIMIT ?" to the query and pass int(limit) as an extra
parameter alongside INGEST_SOURCE_LEGACY_UNKNOWN; keep handling of NULL
content_hash and the same returned tuple format. Reference:
list_canonical_orphans, limit_clause, INGEST_SOURCE_LEGACY_UNKNOWN.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/aelfrice/store.py`:
- Around line 1499-1517: The query in list_canonical_orphans() is read-only and
the file-level write-path guideline doesn't apply; to remove the f-string style
risk while keeping runtime safety, replace the dynamic f-string LIMIT
construction (limit_clause) with a parameterized LIMIT: build the SQL without
interpolated strings and, if limit is provided, append " LIMIT ?" to the query
and pass int(limit) as an extra parameter alongside
INGEST_SOURCE_LEGACY_UNKNOWN; keep handling of NULL content_hash and the same
returned tuple format. Reference: list_canonical_orphans, limit_clause,
INGEST_SOURCE_LEGACY_UNKNOWN.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ce52232-2496-4bf2-9b34-24825b676a74

📥 Commits

Reviewing files that changed from the base of the PR and between c793a5d and f2a4833.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/CHANGELOG.md
📒 Files selected for processing (3)
  • src/aelfrice/replay.py
  • src/aelfrice/store.py
  • tests/test_store_crud.py

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Addressed Sourcery's review on f20288a:

  • Parameterized LIMIT in list_canonical_orphans — was an f-string interpolation of int(limit) (runtime-safe but opengrep-flagged). Now uses bound parameter LIMIT ?.
  • COALESCE(source_kind, '') != ? — declined. ingest_log.source_kind is TEXT NOT NULL (store.py L346), so the NULL-handling divergence from the prior Python logic is not reachable. Not adding defensive code for a schema-enforced invariant.
  • NOT INNOT EXISTS for null-safe je.value — declined for the same reason. derived_belief_ids is list[str] at every call site (record_ingest signature); json_each cannot yield NULL values from a list of non-null strings.

All 9 list_canonical_orphans tests still pass; replay orphan tests still pass.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Newton:2026-05-13T02:07:42Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Newton review — LGTM.

Verdict

Approve. Adding ready-to-merge after this comment; if the bot rejects on the (small) rebase, author can rebase and re-label.

What I checked

  • Spec match. All four perf(replay): replace N+1 belief loop in _compute_replay_drift_report with set-based query #725 acceptance items checked: list_canonical_orphans() method, replay.py consumes it (N+1 loop and TODO(perf): comment removed), test suite passes byte-identical (3764/59/75), bench evidence in PR body.
  • Output stability. New query orders ORDER BY b.id ASC, matching list_belief_ids() order — examples_canonical_orphan sample contents stay deterministic. Replay-soak parity preserved.
  • Commit hygiene. 4 atomic commits, all SSH-signed (G): feat(store) → test(store) → perf(replay) → docs(changelog). Plus the sourcery-prompted f-string fix commit f20288a which parameterized the LIMIT. Discretion grep on diff clean.

On the bot comments

  • Sourcery SQL-injection alert (false positive). Was flagging the old f"...{limit_clause}" shape. Already fixed in f20288a — LIMIT is now sql += " LIMIT ?" with int(limit) bound parameter. No raw concatenation of untrusted input.
  • Sourcery NULL-source_kind concern (not applicable). Schema declares source_kind TEXT NOT NULL (store.py:343). Old Python's str(r.get('source_kind', '')) defensive default was dead code against the schema invariant. No behavior change.
  • Sourcery json_each NULL / NOT-IN quirk (theoretical). derived_belief_ids is a JSON array of belief id strings; null elements would be a data invariant violation upstream, not something to defend against here. The pattern matches the existing synthesize_legacy_log shape in store.py, so consistency wins over defensive rewriting.
  • CodeRabbit f-string nit. Already addressed in f20288a.

Mechanical

@robotrocketscience robotrocketscience added ready-to-merge Trigger merge-train: FF main to this PR's head and removed attn:review Needs review (PR open, awaiting reviewer) labels May 13, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Newton:2026-05-13T02:09:43Z]

@github-actions

Copy link
Copy Markdown

merge-train: blocked

branch is not fast-forward on main (branch base c793a5d2dbce34e40a45987101a557fc1aa34940, current main aaed31437c87dcdc528646a7f23c47bad3760002). Rebase locally (git rebase github/main), force-push, and re-add the label.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 13, 2026
@robotrocketscience
robotrocketscience force-pushed the perf/issue-725-replay-canonical-orphans-set-query branch from f20288a to a7fa14f Compare May 13, 2026 02:31
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 13, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Rebased onto current main (f20288aa7fa14f). Five commits replayed cleanly; CHANGELOG.md sections didn't overlap (main added ### Documentation under [3.0.0] - Unreleased; PR added ### Performance entry farther down). All commits re-signed (G). Discretion grep clean. Local test pass: tests/test_store_crud.py tests/test_replay_full_equality.py 42/42 in 1.3s.

Re-added ready-to-merge.

@github-actions

Copy link
Copy Markdown

merge-train: blocked

required check(s) failed: consecutive-green ≥ 7d. Fix CI and re-add the label.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 13, 2026
Add MemoryStore.list_canonical_orphans(limit) that returns
(belief_id, content_hash) tuples for beliefs where every ingest_log row
has source_kind='legacy_unknown' or no row exists at all.

Single SQL pass: NOT IN (SELECT je.value FROM ingest_log il,
json_each(il.derived_belief_ids) je WHERE source_kind != 'legacy_unknown')
replaces the N+1 per-belief iteration. Results ordered ORDER BY b.id ASC
to preserve sample stability matching list_belief_ids().
Nine tests in test_store_crud.py covering:
- empty store, no log rows (pre-#205), all-legacy rows
- non-legacy row excludes belief
- mixed beliefs (orphans vs non-orphans)
- mixed log rows on same belief (both legacy + non-legacy → not orphan)
- limit= cap, ORDER BY id ASC ordering, content_hash tuple field
…ort (#725)

Replace the N+1 per-belief loop (list_belief_ids + iter_ingest_log_for_belief
per id + get_belief per orphan) with a single call to the new set-based
list_canonical_orphans(). Removes the TODO(perf) comment.

Output is byte-identical: canonical_orphan count and examples_canonical_orphan
sample are produced from the same ordered set, capped at drift_examples.
Add Performance entry under [3.0.0] - Unreleased describing the
list_canonical_orphans optimization and its byte-identical contract.
…ourcery)

opengrep flagged the f-string LIMIT interpolation as a SQL-injection
shape even though int(limit) made it runtime-safe. Switch to bound
parameter for static-analysis cleanliness and consistency with the
rest of the store query layer. No behaviour change.

Sourcery's adjacent suggestions to COALESCE source_kind for NULL
parity and switch NOT IN to NOT EXISTS for NULL-safe je.value are
moot under the current schema: source_kind is TEXT NOT NULL and
derived_belief_ids holds list[str]. Not adding defensive code for
guaranteed invariants.
@robotrocketscience
robotrocketscience force-pushed the perf/issue-725-replay-canonical-orphans-set-query branch from a7fa14f to a8d0b8d Compare May 13, 2026 14:31
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Re-rebased onto current main (a7fa14fa8d0b8d) after #719 landed. Five commits replayed cleanly, all re-signed (G). Tests 42/42 in 4.7s. Soak streak is now 7 — consecutive-green gate should clear on the new SHA.

Re-adding ready-to-merge.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 13, 2026
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 13, 2026
@github-actions

Copy link
Copy Markdown

merge-train: merged a8d0b8dmain via FF push.

@github-actions
github-actions Bot merged commit a8d0b8d into main May 13, 2026
32 of 33 checks passed
@robotrocketscience robotrocketscience mentioned this pull request May 13, 2026
8 tasks
@robotrocketscience
robotrocketscience deleted the perf/issue-725-replay-canonical-orphans-set-query branch May 14, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-schwartzchild Authored by parallel session schwartzchild

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(replay): replace N+1 belief loop in _compute_replay_drift_report with set-based query

1 participant