Skip to content

fix(wonder): mark speculative phantoms, keep them collectable, and content-address their ids (#1171) - #1189

Merged
github-actions[bot] merged 8 commits into
mainfrom
fix/issue-1171-speculative-phantoms
Jul 30, 2026
Merged

fix(wonder): mark speculative phantoms, keep them collectable, and content-address their ids (#1171)#1189
github-actions[bot] merged 8 commits into
mainfrom
fix/issue-1171-speculative-phantoms

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Closes #1171.

All four findings confirmed by execution before any code changed. The audit filed one of them as PLAUSIBLE; it holds.

gc candidates BEFORE retrieval: ['phan0000000000aa']
record_retrieval rows written: 2
phantom posterior after exposure: alpha=0.3000 beta=1.0000   <- posterior NOT moved
gc candidates AFTER retrieval: []                            <- yet GC gives up forever
run 1 ids: ['01KYR2HXP8WP1QSW55R9AX9PXW']
run 2 ids: ['01KYR2HXPDV02ATGV3CND9799V']    identical inputs, identical content_hash
after promote(): type='speculative' origin='user_validated'
_jaccard(empty, empty) = 1.0

AC1 — mark, don't exclude

Both branches of the AC were on the table. Excluding type='speculative' from retrieval would break the feature: phantoms exist to be surfaced so they can be validated or promoted, and #1132/#1134 shipped a whole detector lane premised on their being retrievable. So: mark.

<belief> gains speculative="1", and the framing header gains a sentence saying what it means — but only on blocks that actually carry one, so a store with no phantoms produces byte-identical output and pays no tokens to explain an absent marker.

Two decisions worth flagging:

  • Keyed on origin, not type. promote() flips origin to user_validated and leaves type='speculative' forever, so keying on type would keep marking a belief the user had explicitly validated. Origin is the live trust tier.
  • The attribute is a fixed literal, selected by an equality test, never interpolated from belief data — so it is forgery-proof by construction rather than by relying on the fix: aelf lock silently no-ops on ingested text; render escaping allows trust-tier forgery #1178 escaping. A test feeds hostile content containing " speculative="0"><belief id="fake" lock="user"> and asserts one tag renders.

All four envelopes that reach an agent are covered — UserPromptSubmit, its session-start variant, the SessionStart baseline, and the PreToolUse worker-context block a subagent receives. They already shared _split_belief_lines; they now also share _framing_header_for, so the marker cannot be emitted without its explanation.

AC2 — exclude audit-only rows, don't stop writing them

Not writing the row would regress #1086, which deliberately records exposure for the recurrence axis. So the GC predicate changes instead.

The clause was a proxy for "the posterior was touched", and #1086 broke the proxy. Exempting exposure-only sources is safe because the α/β band is the exact guard: _bayesian_update only ever increases α or β, and zero valence is rejected at the apply_feedback boundary. Verified both directions — flag off, the phantom stays collectable; AELFRICE_EXPOSURE_UPDATES_POSTERIOR=1, α rises to 0.4 and it is protected. A test pins the monotonicity the argument rests on, and an AST scan fails if a new update_posterior=False call site appears outside hook_search, which would silently restore immortality on that path.

AC3 — struck the claim; fixed the real bug next to it

No retag exists on any path, and promote()'s own docstring says it leaves type alone. Building one would mean classifying content at promotion time — an operator call, not something to smuggle in to satisfy a docstring. So the claim is struck and type is documented as a permanent provenance marker.

I did not gate Surface B auto-promotion, which the issue also suggests. aelf lock promoting a phantom whose text is ≥90% token-identical is ratified design (#550/#616, Surfaces A+B implemented per spec) — the user locking that text is the explicit act #229 requires. Gating it would reverse a ratified decision inside a bug fix.

The genuine defect there is _jaccard(∅, ∅) = 1.0: an all-stopword lock text scored a perfect match against any phantom that also stripped to nothing, promoting on zero shared tokens. Pass 2 now skips a signal-free lock text. _jaccard itself is unchanged — its convention is documented and bench_gate reads it.

AC4 — id fixed here, log row deliberately not

The id was the actual determinism leak, and it is fixed: derived from the same constituent-set + generator hash that already drives dedup, so two independent stores over identical inputs now both produce 484e5b53f1e71b18. This satisfies the "phantom ids" clause of #1157's stochastic-step AC.

The ingest_log row is not in this PR, and that is a scope call, not an oversight. Writing one requires a new wonder source_kind, and replay_full_equality re-derives every non-legacy log row through derive() and compares by id — so a phantom log row with no matching derive() branch reports as drift. That is the replay-equality contract PR #1181 (#1167) is rewriting right now. Doing both at once means a conflict or an unreviewable diff.

Worth stating plainly: the derivation is already auditable without the log row — RELATES_TO edges to every constituent plus a wonder_ingest corroboration row carrying <generator>@<score>. What was broken was that the phantom's identity was random, so the same derivation produced a differently-shaped graph each run. That is closed. The remaining gap is write-trail completeness, which is hole #1 of umbrella #1157 ("the log is not total") — I'll annotate that umbrella rather than open a duplicate.

Verification

6175 passed, 69 skipped, 71 xfailed, 4 xpassed — 34 new tests, up from 6141.

Falsified per defect by reverting one fix at a time, keeping new symbols importable so failures are behavioural rather than ImportError:

reverted tests that fail
GC exemption 3
phantom id 4
injection marker 6
zero-signal lock match 5

No cross-contamination beyond the deliberate end-to-end case. My first attempt at this was wrong and I caught it: the naive revert left 4 bind params against 3 placeholders, so three tests were failing on a broken SQL statement rather than on the bug. Once the revert was faithful those three passed both ways — correctly, since they are guard tests asserting what the fix must not change (endorsement feedback still protects; a genuine lock match still promotes).

A new hazard the deterministic id creates is tested too: primary-key reuse after a GC reap was unreachable while ids were random, and get_belief_by_content_hash does not filter valid_to, so re-ingest correctly skips instead of raising.

Summary by Sourcery

Address multiple defects in the speculative phantom lifecycle, improving trust signalling, garbage collection eligibility, and identity determinism, and update documentation and tests accordingly.

Bug Fixes:

  • Mark speculative, wonder-generated phantoms in all hook injection envelopes and explain their meaning in the framing header so conjecture is distinguished from user assertions.
  • Fix wonder garbage collection to ignore audit-only exposure feedback rows while still protecting phantoms that receive genuine endorsement or posterior-updating exposure.
  • Derive phantom belief IDs deterministically from their constituent set and generator so identical derivations share an ID across runs and stores, while remaining compatible with existing ULID-based rows.
  • Prevent zero-signal lock texts (that normalize to no tokens) from promoting speculative phantoms via an empty/empty Jaccard match and clarify that promotion preserves the speculative type marker.

Enhancements:

  • Document speculative phantom semantics as a permanent provenance marker keyed on origin for trust tier, and extend hook hardening and philosophy docs to describe the speculative marker and its role in the trust boundary.

Documentation:

  • Update hook hardening and philosophy documentation to describe the speculative marker, its framing header explanation, and how it separates machine-synthesised conjecture from user assertions.
  • Add detailed changelog entry describing the speculative phantom lifecycle fixes, their prior failure modes, and remaining limitations around ingest logging.

Tests:

  • Add a comprehensive speculative phantom trust test suite covering injection marking, GC behaviour with exposure-only feedback, Bayesian monotonicity, phantom ID determinism, and promotion semantics, including static checks guarding against new audit-only feedback call sites.

wonder_gc excluded any phantom with a feedback_history row, as a proxy
for "the posterior was touched". #1086 broke the proxy: every retrieval
writes an audit-only row without moving the posterior, so one hook
surfacing made a phantom permanently uncollectable at its ingest prior
and the 14-day TTL never fired again.

Exempt EXPOSURE_ONLY_FEEDBACK_SOURCES from the row clause. Safe because
the alpha/beta band is the exact guard — _bayesian_update only ever
increases alpha or beta and zero valence is rejected at the boundary, so
genuine endorsement still pushes the belief out of the band. Verified
both ways: flag off, the phantom stays collectable; flag on, alpha rises
to 0.4 and it is protected.
… urandom

Phantoms were the only beliefs in the store with an entropy-derived
primary key: ulid() is time.time() plus os.urandom(10). Two wonder runs
over an identical store minted different ids for the same derivation, so
the RELATES_TO subgraph came out a different shape each time and the
question "how did this store get from belief A to phantom P" had no
answer from identity.

Derive the id from _constituent_key -- the same (constituent set,
generator) hash that already drives dedup -- so id and content_hash are
two views of one fact. Verified: two independent stores over identical
inputs now produce id 484e5b53f1e71b18 in both. Pre-existing phantoms
keep their ULIDs; dedup is on content_hash, so a mixed-id store neither
duplicates nor collides.
A wonder-synthesised phantom rendered byte-identically to a belief the
user actually said, so machine conjecture reached the agent as ordinary
retrieved context with nothing to distinguish it. Add speculative="1" to
the <belief> line and a framing sentence that says what it means.

The attribute is a fixed literal selected by an equality test, never
interpolated from belief data, so content cannot forge it. Keyed on
origin rather than type: promotion flips origin to user_validated while
type stays 'speculative' forever, so a user-validated phantom correctly
loses the marker.

All four envelopes -- UserPromptSubmit, the session-start-embedded
variant, the SessionStart baseline, and the PreToolUse worker-context
block a subagent receives -- now route their header through
_framing_header_for, so the marker is never emitted without its
explanation. Blocks with no phantom stay byte-identical to pre-#1171.
…antom

_jaccard returns 1.0 for the empty/empty pair by convention, so a lock
text that normalized to no tokens scored a perfect match against any
phantom whose content also stripped to nothing -- silently promoting it
to user_validated on the strength of zero shared tokens. Skip pass 2 when
the lock text carries no signal.

Also strike the "C4 retags this" claim from BELIEF_SPECULATIVE. No retag
exists on any path and promote() documents that it leaves type alone;
`type` is a permanent provenance marker and `origin` is the trust tier.
Building the retag instead would mean classifying content at promotion
time, which is an operator call, not a docstring's to make.
34 tests across the four #1171 defects, each falsified against pre-fix
code by reverting one fix at a time: GC exemption 3 failures, phantom id
4, injection marker 6, zero-signal lock match 5, with no cross-defect
contamination beyond the deliberate end-to-end case.

Three tests assert behaviour the fixes must NOT change and so pass both
ways: endorsement feedback still protects a phantom, an exposure row that
does move the posterior still protects it, and a genuine lock match still
promotes (Surface B auto-promotion is ratified design).

Two guard the reasoning rather than the code. The monotonicity test pins
the argument the GC predicate now rests on -- exempting exposure sources
is only safe because the alpha/beta band catches every posterior-moving
event. The AST scan fails if a new update_posterior=False call site
appears outside hook_search, which would silently restore immortality on
that path with nothing else in the suite noticing.
PHILOSOPHY's trust-boundary section counts the structural defenses, so
the new marker belongs in that count. hook_hardening.md is the memo that
specifies the rendered shape; its contract table pointed at two
formatters when four now share _split_belief_lines.
@robotrocketscience robotrocketscience added the author-Setr PR coordination mutex label Jul 29, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Marks speculative wonder phantoms in all hook injection envelopes, fixes GC eligibility to keep purely-exposed phantoms collectable, makes phantom ids content-addressed and deterministic, corrects speculative type/provenance semantics, and hardens promotion against zero-signal lock matches, with extensive tests and docs updates around these behaviors.

Sequence diagram for speculative phantom marking in hook injection

sequenceDiagram
    participant UserPromptSubmit
    participant hook_format_hits as _format_hits
    participant hook_split as _split_belief_lines
    participant hook_header as _framing_header_for

    UserPromptSubmit->>hook_format_hits: _format_hits(hits)
    hook_format_hits->>hook_split: _split_belief_lines(hits)
    hook_split-->>hook_format_hits: belief_lines, manifest_lines
    hook_format_hits->>hook_header: _framing_header_for(hits)
    hook_header-->>hook_format_hits: header

    rect rgb(230,230,255)
        Note over hook_split: For each Belief h in hits
        hook_split->>hook_split: content = _escape_for_hook_block(h.content)
        alt h.origin == ORIGIN_SPECULATIVE
            hook_split->>hook_split: speculative_attr = ' speculative="1"'
        else
            hook_split->>hook_split: speculative_attr = ''
        end
        hook_split-->>hook_split: '<belief id=... lock=...'+speculative_attr+'>'
    end

    rect rgb(230,255,230)
        alt any(h.origin == ORIGIN_SPECULATIVE)
            hook_header-->>hook_header: return _FRAMING_HEADER + _SPECULATIVE_FRAMING_SENTENCE
        else
            hook_header-->>hook_header: return _FRAMING_HEADER
        end
    end

    hook_format_hits-->>UserPromptSubmit: rendered <aelfrice-memory> block
Loading

File-Level Changes

Change Details Files
Mark speculative phantoms on injection and explain their trust tier in all agent-facing envelopes.
  • Extend hook splitting to add a fixed speculative="1" attribute on beliefs with speculative origin.
  • Introduce a shared framing_header_for() helper that conditionally appends a speculative explanation sentence when hits contain speculative origins.
  • Wire framing_header_for() into all injection paths: normal hits, hits with session start, baseline session start, and worker-context blocks.
  • Add tests ensuring phantoms are marked, asserted beliefs are not, marker/explanation are coupled, hostile content cannot forge the attribute, and blocks without phantoms remain byte-identical.
src/aelfrice/hook.py
src/aelfrice/hook_agent_context.py
docs/design/hook_hardening.md
docs/concepts/PHILOSOPHY.md
tests/test_speculative_phantom_trust.py
Adjust wonder GC to ignore exposure-only feedback rows while retaining endorsement protection via the posterior band.
  • Introduce EXPOSURE_ONLY_FEEDBACK_SOURCES constant to classify feedback sources that record exposure without updating posterior.
  • Change query_wonder_gc_candidates to add a NOT IN filter on feedback_history.source for exposure-only sources, while relying on alpha/beta prior band as the exact guard for posterior-moving events.
  • Handle an empty EXPOSURE_ONLY_FEEDBACK_SOURCES set without emitting invalid SQL by eliding the NOT IN clause.
  • Add tests asserting exposure-only retrievals leave phantoms collectable, endorsement feedback still protects, exposure-with-posterior-updates stays protected, bayesian updates are monotone, exposure-only sources track hook_search, and no new update_posterior=False callsites appear outside hook_search.
src/aelfrice/store.py
src/aelfrice/models.py
tests/test_speculative_phantom_trust.py
Make phantom belief ids deterministic, content-addressed from constituent-set + generator, aligned with the global belief-id scheme.
  • Replace ulid()-based phantom id generation with _phantom_belief_id() using the constituent_key hash truncated to a shared 16-hex width.
  • Document phantom id determinism and its relationship to content_hash and RELATES_TO graph shape in lifecycle comments.
  • Ensure existing ULID phantoms remain valid while dedup continues to operate on content_hash.
  • Add tests that two identical derivations across stores share one id, ids are independent of content/score but differ for differing derivations, constituent order does not affect id, phantom id length matches derivation-wide belief id length, phantom id prefixes its content hash, and regenerating a GC-reaped phantom dedups rather than collides.
src/aelfrice/wonder/lifecycle.py
tests/test_speculative_phantom_trust.py
Clarify speculative type/provenance semantics and prevent promotion on zero-signal lock texts.
  • Update BELIEF_SPECULATIVE documentation to state type as a permanent provenance marker and origin as the trust tier, striking the nonexistent retag claim.
  • Modify find_phantom_lock_matches to skip Jaccard pass entirely when normalized lock text yields no tokens, avoiding empty/empty=1.0 promotion.
  • Add tests that signal-free lock texts match no phantoms, genuine text matches still promote, and promotion leaves type='speculative' while flipping origin to user_validated.
src/aelfrice/models.py
src/aelfrice/promotion.py
tests/test_speculative_phantom_trust.py
Update changelog and design/docs to reflect speculative phantom fixes and trust-boundary behavior.
  • Add a detailed v4 changelog entry summarizing the four speculative phantom defects and their fixes, and calling out the non-scoped ingest_log change.
  • Extend hook hardening and philosophy docs to describe speculative="1" semantics, their non-forgeable implementation, and the additional structural defense around machine conjecture vs asserted beliefs.
CHANGELOG/v4.md
docs/design/hook_hardening.md
docs/concepts/PHILOSOPHY.md

Assessment against linked issues

Issue Objective Addressed Explanation
#1171 Ensure speculative phantom beliefs are distinguished from evidenced content in injected retrieval blocks (either by excluding them from default retrieval or by marking them distinctly).
#1171 Prevent a single retrieval exposure from making speculative phantoms permanently exempt from wonder-GC (either by not writing feedback_history when update_posterior=False, or by excluding audit-only feedback rows from the GC predicate).
#1171 Align promotion behaviour and documentation for speculative phantoms, including clarifying that type='speculative' is a permanent provenance marker and fixing the zero-signal lock text auto-promotion bug.
#1171 Make phantom belief IDs deterministically derived from their constituent set and generator, and add a corresponding ingest log row so their creation is fully replayable and auditable. The PR changes phantom IDs to be deterministically derived from the constituent key (content-addressed, matching the store-wide belief ID shape), but it explicitly does not add an ingest_log row for phantom creation. The write trail remains incomplete and the author notes that routing phantoms through the log is deferred to a separate change.

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

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robotrocketscience, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 56e4d476-c07f-491a-862e-0d9933c3d846

📥 Commits

Reviewing files that changed from the base of the PR and between c2b4e28 and ca97776.

📒 Files selected for processing (10)
  • CHANGELOG/v4.md
  • docs/concepts/PHILOSOPHY.md
  • docs/design/hook_hardening.md
  • src/aelfrice/hook.py
  • src/aelfrice/hook_agent_context.py
  • src/aelfrice/models.py
  • src/aelfrice/promotion.py
  • src/aelfrice/store.py
  • src/aelfrice/wonder/lifecycle.py
  • tests/test_speculative_phantom_trust.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.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 709 changed lines (limit: 200)
  • 10 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 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/test_speculative_phantom_trust.py" line_range="167-173" />
<code_context>
+        assert (alpha, beta) != (b.alpha, b.beta)
+
+
+def test_exposure_only_sources_match_their_owning_module() -> None:
+    """The literal in models.py must track the constant that produces it.
+
+    `models.py` holds the set as a literal to stay free of intra-package
+    imports; this is the seam that keeps the two from drifting.
+    """
+    assert hook_search.HOOK_FEEDBACK_SOURCE in EXPOSURE_ONLY_FEEDBACK_SOURCES
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a test that covers the `EXPOSURE_ONLY_FEEDBACK_SOURCES` empty-set branch in the GC predicate.

`query_wonder_gc_candidates` has a defensive path for an empty `EXPOSURE_ONLY_FEEDBACK_SOURCES` (falling back to the pre-#1171 behavior and avoiding `NOT IN ()`), but current tests only cover the non-empty case. Please add a test that monkeypatches `EXPOSURE_ONLY_FEEDBACK_SOURCES` to an empty `frozenset`, runs GC, and asserts both that the query executes successfully and that a speculative belief is still reaped, to lock in this branch and prevent SQL-shape regressions.

Suggested implementation:

```python
        assert alpha >= b.alpha
        assert beta >= b.beta
        assert (alpha, beta) != (b.alpha, b.beta)


def test_gc_handles_empty_exposure_only_feedback_sources(monkeypatch, wonder_db_session) -> None:
    """GC must still reap speculative beliefs when EXPOSURE_ONLY_FEEDBACK_SOURCES is empty.

    `query_wonder_gc_candidates` has a defensive branch that avoids generating
    a `NOT IN ()` clause when `EXPOSURE_ONLY_FEEDBACK_SOURCES` is empty; this test
    exercises that branch and ensures the query shape remains valid while
    speculative beliefs are still collected.
    """
    # Ensure the GC predicate sees an empty exposure-only set, matching the
    # pre-#1171 behaviour.
    monkeypatch.setattr(
        models,
        "EXPOSURE_ONLY_FEEDBACK_SOURCES",
        frozenset(),
        raising=True,
    )

    # Create / retrieve at least one speculative belief that is eligible for GC.
    # This mirrors the setup used by the existing GC tests to produce a
    # speculative phantom candidate.
    speculative_belief = _belief(
        "speculative_gc_candidate",
        "content",
        alpha=0.1,
        beta=0.1,
        speculative=True,
    )

    # The GC candidate query should execute successfully even with an empty
    # EXPOSURE_ONLY_FEEDBACK_SOURCES, and it must still consider speculative
    # beliefs for reaping.
    candidates = list(query_wonder_gc_candidates(wonder_db_session))
    assert any(candidate.id == speculative_belief.id for candidate in candidates)

    # Optionally, if the test suite includes a helper that actually performs
    # the GC side-effect (deleting rows), we can invoke it here to assert that
    # the speculative belief is reaped and no longer present after GC runs.
    # This keeps the behaviour aligned with the pre-#1171 semantics.
    #
    # run_wonder_gc(wonder_db_session)
    # assert not wonder_db_session.get(models.Belief, speculative_belief.id)


Four independent defects on the phantom lifecycle, each verified here against

```

To make this compile and integrate cleanly with the rest of the test suite, you will likely need to:

1. Ensure `models` and `query_wonder_gc_candidates` are imported at the top of `tests/test_speculative_phantom_trust.py`:
   - `from wonder import models`
   - `from wonder.gc import query_wonder_gc_candidates` (or the appropriate module where it actually lives).
2. Align the fixture name `wonder_db_session` with your existing session fixture. If your tests use a different fixture name (e.g. `db_session`, `session`, or `wonder_session`), adjust the parameter accordingly.
3. Replace the `_belief(...)` construction with the existing helper or fixture used elsewhere in this file to create a speculative GC-eligible belief (for example, a factory like `make_speculative_belief(wonder_db_session)` or a fixture such as `speculative_phantom`). The important properties are:
   - It is a speculative/phantom belief.
   - It is in a state that the existing GC tests already confirm is reaped.
4. If your GC pipeline is driven by a helper (e.g. `run_wonder_gc(wonder_db_session)` or similar), uncomment and point the `run_wonder_gc` call to the correct helper and adjust the final assertion to match how you usually verify that a belief has been reaped (e.g. via ORM `get`, raw SQL, or a higher-level domain helper).
5. If the GC predicate reads `EXPOSURE_ONLY_FEEDBACK_SOURCES` from a module other than `models` (for example, `wonder.models`, `wonder.gc_models`, or similar), update the `monkeypatch.setattr` target module to be the one actually referenced in `query_wonder_gc_candidates`.
</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 tests/test_speculative_phantom_trust.py
Comment thread tests/test_speculative_phantom_trust.py Fixed
Comment thread tests/test_speculative_phantom_trust.py Fixed
Comment thread tests/test_speculative_phantom_trust.py Fixed
…mbdas

Review asked for coverage of the empty EXPOSURE_ONLY_FEEDBACK_SOURCES
branch. Writing it showed my justification for that branch was wrong:
SQLite accepts an empty IN list and reads NOT IN () as always-true, so
both arms behave identically and removing the branch leaves the new test
green. The comment now says that, and the branch stays only because an
empty IN list is a SQLite extension rather than standard SQL.

The suggested patch would have monkeypatched aelfrice.models; store.py
binds the constant at import time, so it would have passed while
exercising nothing. Patched on aelfrice.store instead.

Also collapses three parametrize lambdas that just forwarded their
argument (CodeQL). The fourth binds a second argument and stays.
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 29, 2026
@github-actions

Copy link
Copy Markdown

merge-train: blocked

required check(s) failed: Sourcery review. 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 Jul 29, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

The single finding is addressed in 14b7498 (new test for the empty-exemption-set branch, plus the three redundant parametrize lambdas collapsed for CodeQL). Requesting a fresh pass so the check reflects the current head.

…g an IN list

Sourcery's blocking security finding was the interpolated
`IN ({placeholders})` f-string, not the testing note. Rather than suppress
it, remove the dynamic SQL: the exemption set now arrives as one bound
JSON array read through json_each, the same mechanism the
log-reachability queries already use.

The SQL text is fully static, placeholder and parameter counts can no
longer drift, and the empty-set case needs no branch at all --
json_each('[]') yields no rows so the NOT IN becomes a no-op, which is
the pre-#1171 behaviour the removed branch was there to spell out. That
also disposes of a comment whose stated reason for the branch was wrong.
No behaviour change; 6176 tests still pass.
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 30, 2026
@github-actions
github-actions Bot merged commit ca97776 into main Jul 30, 2026
28 checks passed
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 30, 2026
@github-actions

Copy link
Copy Markdown

merge-train: merged ca97776main via FF push.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-Setr PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(wonder): speculative phantoms are injected unmarked and one retrieval makes them immortal

2 participants