Skip to content

fix(retrieval): demote or exclude superseded beliefs, both arms behind a flag (#1187) - #1191

Merged
github-actions[bot] merged 6 commits into
mainfrom
fix/issue-1187-supersession-demote
Jul 30, 2026
Merged

github-actions[bot] merged 6 commits into
mainfrom
fix/issue-1187-supersession-demote

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Closes #1187. The retrieval half of #1170, split out because it changes retrieve() output on the default path.

Builds both arms behind one default-OFF flag, per the shape you ratified on 2026-07-29. Nothing here presumes a winner; the three-arm bench picks the default.

The defect, reproduced

grep SUPERSED src/aelfrice/retrieval.py returns zero hits — retrieval had no notion of supersession — and uri_baki.apply_supersession_demote had no importer anywhere in src/. Measured on this branch's reproduction:

rank 0: 'deploy target is heroku'  [SUPERSEDED]
rank 1: 'deploy target is fly.io'  [current]

What ships

Knob Default Meaning
use_supersession_demote false lane on/off
supersession_treatment demote demote | exclude
supersession_demote_factor 0.5 demote arm strength

Both resolve env > kwarg > TOML > default, mirroring use_entity_persist_demote. One batched SELECT DISTINCT dst … WHERE type='SUPERSEDES' over the candidate set, via the json_each binding idiom from ca97776 rather than an interpolated IN list. Exclusion is applied before the heat-kernel seeds are computed, so a retired belief does not seed the graph lane either. Threaded through _l1_hitsretrieve_with_tiersretrieve_v2retrieve(), resolver-driven, so the lane is reachable from the production path — the gap that left the primitive importerless.

Two findings you should read before commissioning the bench

1. The issue's suggested wiring would have inverted the fix. #1187 says to give uri_baki.apply_supersession_demote a real importer for the demote arm. That primitive multiplies: score * factor. But the composite rerank score is a log-domain quantity from combine_log_scores / partial_bayesian_score and is routinely negative — measured -13.08 on the two-belief reproduction. Multiplying -13.08 by 0.5 gives -6.54, which is higher: it would have promoted the superseded belief to the top of the pack.

So the demote is additive: s += log(factor). That is the log-domain equivalent of scaling a probability by factor, so your "factor 0.5" semantics survive exactly, and it matches _entity_persist_penalty, which is log-additive for the same reason. uri_baki.apply_supersession_demote is left unimported — its contract is correct only on a non-negative score scale, which is not what this rerank produces. A test asserts the negative-score premise directly so nobody folds the addition back into a multiplication. Per the issue, the primitive is a delete candidate if the bench picks exclusion; I have not deleted it, since that decision is downstream of the bench.

2. At factor 0.5 the demote arm is weak enough that composition can cancel it. Measured on the reproduction with the default-ON entity-persistence lane:

belief entity penalty supersession penalty
fly.io (current) −0.6911 (S1 = 0.5) 0
heroku (superseded) 0 (no entities) −0.6931

The two nearly cancel, the pre-existing bm25 gap survives, and the order does not change. log(0.5) = −0.69 is the same order of magnitude as the entity penalty and far weaker than its log(ε) = −6.91 floor. The demote is applied correctly — verified in isolation — it is simply a weak term in composition.

Recommendation: the bench should sweep supersession_demote_factor, not test 0.5 alone. A three-arm demote-vs-exclude-vs-control run pinned at 0.5 risks reading "demotion doesn't work" when what it measured was "0.5 is too small to outrun a co-resident lane."

Verification

  • Full suite: 6202 passed, 69 skipped (26 new tests). deptry clean; vulture reports only the pre-existing ingest.py:113.
  • Both arms confirmed through retrieve_v2, not just the primitive.
  • Default-off is byte-identical: a test monkeypatches superseded_belief_ids to raise and asserts the lane-off path never calls it, so the short-circuit that skips the rerank stays reachable.

Out of scope

Unrelated observation

tests/test_promotion_adversarial.py C6-01…C6-04 now xpass on bare main (verified at ca97776, independent of this branch): #1189's all-stopword promotion fix made them pass without un-marking the xfail. Harmless today, but the markers now hide a working guard rather than a known bug. Not touched here.

Summary by Sourcery

Introduce a configurable supersession lane in retrieval that can demote or exclude beliefs marked as superseded, wired through the production retrieval path but shipped behind a default-off flag.

New Features:

  • Add supersession retrieval controls (use_supersession_demote, supersession_treatment, supersession_demote_factor) with env/TOML/kwarg resolution to support demote or exclude behaviour for superseded beliefs.
  • Expose a store-level helper to retrieve superseded belief ids via a batched SUPERSEDES edge query over the candidate set.

Enhancements:

  • Integrate supersession handling into L1 reranking for both BM25F and FTS5 paths, including additive log-domain penalties and pre-seed exclusion of retired beliefs.
  • Extend retrieval configuration documentation to describe the supersession lane knobs and precedence, and note their impact on default retrieval behaviour.
  • Update the v4 changelog to record the supersession retrieval fix and its default-off, bench-gated rollout shape.

Tests:

  • Add comprehensive tests for the supersession lane, covering store edge queries, demote vs exclude behaviour through _l1_hits and retrieve_v2, resolver precedence, factor clamping, and interaction with entity-persistence demotion.

Summary by CodeRabbit

  • New Features
    • Added optional supersession-aware retrieval to handle beliefs marked as superseded.
    • Superseded beliefs can be either demoted in ranking or excluded from results, configurable via retrieval settings.
    • Added env/TOML/parameter support with safe defaults, clamping, and normalization of treatment values.
  • Bug Fixes
    • Fixed an issue where retired/superseded beliefs could incorrectly outrank newer beliefs after supersession.
  • Documentation
    • Documented the new retrieval configuration options and behavior.
  • Tests
    • Added end-to-end tests for both treatments, scoring behavior, precedence, and edge cases.

@robotrocketscience robotrocketscience added the author-Kulili PR coordination mutex label Jul 30, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a supersession-aware retrieval lane that can either demote or exclude beliefs retired by SUPERSEDES edges, wires it through the production retrieval path behind a default-off flag with env/kwarg/TOML resolution, adds a batched store query and configuration/docs/changelog updates, and thoroughly tests both arms and resolver behavior.

Sequence diagram for supersession-aware retrieval lane

sequenceDiagram
    actor User
    participant Client
    participant retrieve_v2
    participant retrieve_with_tiers
    participant _l1_hits
    participant Store

    User->>Client: call retrieve()
    Client->>retrieve_v2: retrieve_v2(query, use_supersession_demote, supersession_treatment, supersession_factor)
    retrieve_v2->>is_supersession_demote_enabled: use_supersession_demote
    retrieve_v2->>resolve_supersession_treatment: supersession_treatment
    retrieve_v2->>resolve_supersession_factor: supersession_factor
    retrieve_v2->>retrieve_with_tiers: use_supersession_demote, supersession_treatment, supersession_factor
    retrieve_with_tiers->>_l1_hits: use_supersession_demote, supersession_treatment, supersession_factor

    alt use_supersession_demote
        _l1_hits->>Store: superseded_belief_ids(candidate_ids)
        Store-->>_l1_hits: superseded_ids
        alt supersession_treatment == SUPERSESSION_TREATMENT_EXCLUDE
            _l1_hits->>_l1_hits: filter out superseded beliefs
        else supersession_treatment == SUPERSESSION_TREATMENT_DEMOTE
            _l1_hits->>_supersession_penalty: superseded_ids, belief_id, supersession_factor
            _supersession_penalty-->>_l1_hits: log(factor) penalty
            _l1_hits->>_l1_hits: s += _supersession_penalty(...)
        end
    else not use_supersession_demote
        _l1_hits-->>retrieve_with_tiers: ranking unchanged by supersession
    end

    retrieve_with_tiers-->>retrieve_v2: ranked beliefs
    retrieve_v2-->>Client: results
    Client-->>User: surface ranked beliefs
Loading

Flow diagram for supersession treatment demote vs exclude

flowchart TD
    A["Supersession lane enabled?"] -->|No| B["Return original ranking"]
    A -->|Yes| C["Fetch superseded_belief_ids from Store"]
    C --> D["supersession_treatment"]
    D -->|SUPERSESSION_TREATMENT_EXCLUDE| E["Filter out superseded beliefs from candidate set"]
    D -->|SUPERSESSION_TREATMENT_DEMOTE| F["Compute penalty = log(clamped supersession_factor)"]
    F --> G["Add _supersession_penalty to rerank score for superseded beliefs"]
    E --> H["Continue retrieval with pruned candidates"]
    G --> H
    H --> I["Return supersession-aware ranking"]
Loading

File-Level Changes

Change Details Files
Add a supersession-aware rerank lane to L1 retrieval that can demote or exclude beliefs retired by SUPERSEDES edges, fully wired through retrieve_v2/retrieve and guarded by a default-off flag.
  • Introduce supersession configuration constants and env-var names, including lane flag, treatment selector, and demote factor with safety bounds.
  • Add env/kwarg/TOML resolvers for the supersession lane enablement, treatment, and factor, matching precedence used by other retrieval knobs.
  • Implement a log-additive supersession penalty helper that demotes superseded beliefs without ever promoting or producing non-finite scores.
  • Extend _l1_hits on both BM25F and FTS5 paths to compute superseded belief ids once per retrieval, apply exclusion before downstream scoring/heat-kernel, and apply the demote penalty during rerank.
  • Thread supersession lane parameters through retrieve_with_tiers and retrieve_v2, resolving them via the new helpers so the lane is reachable from the production path while remaining default-off.
src/aelfrice/retrieval.py
Expose superseded belief-resolution in the store via a batched SUPERSEDES edge query over a candidate set.
  • Add EDGE_SUPERSEDES import to the store module to support supersession-aware queries.
  • Implement MemoryStore.superseded_belief_ids that returns the subset of a candidate id list that appear as dst in SUPERSEDES edges, using a single JSON-bound SELECT DISTINCT with json_each to avoid dynamic IN lists.
  • Ensure empty candidate sets short-circuit without SQL and duplicate ids collapse via DISTINCT so the helper scales to typical rerank workloads.
src/aelfrice/store.py
Document the supersession lane configuration knobs and behavior, and record the retrieval bugfix in the changelog.
  • Extend CONFIG.md’s [retrieval] section to describe use_supersession_demote, supersession_treatment, and supersession_demote_factor, including defaults, precedence, and the demote-vs-exclude semantics.
  • Clarify that the demote factor is applied additively in log-space, clamped to (0,1], and that exclusion operates before heat-kernel seeding.
  • Add a v4 changelog entry explaining the original defect (superseded beliefs never demoted/excluded), the new lane design, additive penalty rationale, and calibration notes for the supersession factor and its interaction with entity persistence.
docs/user/CONFIG.md
CHANGELOG/v4.md
Add a dedicated test suite for the supersession lane covering store-level edge handling, penalty semantics, L1 rerank behavior for both arms, resolver precedence, and production-path wiring via retrieve_v2.
  • Create tests that verify superseded_belief_ids returns dst-side ids only, is scoped to the candidate set, ignores non-SUPERSEDES edge types, handles empty/duplicate inputs, and uses the canonical src/dst direction.
  • Assert _supersession_penalty is log(factor), never positive/promoting, remains finite at factor zero, and that a multiplicative demote on negative scores would invert the intended behavior.
  • Exercise _l1_hits control vs demote vs exclude arms (including empty-pack cases) and ensure the lane-off path does not call superseded_belief_ids, preserving default-path behavior and performance.
  • Test entity-persistence and supersession demotes in combination to capture their additive composition and potential cancellation at factor 0.5.
  • Validate resolver defaults and precedence for lane enablement, treatment, and factor, including normalization, clamping, and stderr-traced error cases, and confirm retrieve_v2 threads the lane through so it affects production retrieval only when explicitly enabled.
tests/test_supersession_lane.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1187 Add a supersession-aware retrieval lane that demotes or excludes beliefs targeted by SUPERSEDES edges, controlled by configuration (env/kwarg/TOML), default-off, and fully wired through the production retrieve()/retrieve_v2 pipeline.
#1187 Implement supersession detection using a single batched query over the candidate set (SELECT DISTINCT dst FROM edges WHERE type='SUPERSEDES' AND dst IN ...), exposed via the store API and used in the L1 rerank.
#1187 Document the supersession lane configuration knobs and behaviour (use_supersession_demote, supersession_treatment, supersession_demote_factor) in user-facing docs and changelog.

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 Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 29 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: 36cf5dd8-1a0f-406d-abf3-d67d04f782ee

📥 Commits

Reviewing files that changed from the base of the PR and between a3e2295 and fc6f736.

📒 Files selected for processing (5)
  • CHANGELOG/v4.md
  • docs/user/CONFIG.md
  • src/aelfrice/retrieval.py
  • src/aelfrice/store.py
  • tests/test_supersession_lane.py
📝 Walkthrough

Walkthrough

The retrieval pipeline adds a default-off supersession lane. It resolves configuration from environment, kwargs, TOML, and defaults; identifies superseded candidates through a batched store query; then demotes or excludes them during L1 scoring.

Changes

Supersession retrieval

Layer / File(s) Summary
Supersession configuration resolution
src/aelfrice/retrieval.py
Adds lane constants, environment/TOML/kwarg resolution, treatment validation, factor clamping, and finite log-domain penalties.
Superseded candidate lookup
src/aelfrice/store.py
Adds a single-query MemoryStore.superseded_belief_ids() lookup for SUPERSEDES destinations within the candidate set.
L1 retrieval scoring and wiring
src/aelfrice/retrieval.py
Passes supersession settings through retrieval entry points and applies demotion or exclusion in BM25F and FTS5 paths.
Supersession behavior validation and documentation
tests/test_supersession_lane.py, docs/user/CONFIG.md, CHANGELOG/v4.md
Adds regression coverage and documents configuration, precedence, treatment modes, and defaults.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant RetrieveV2
  participant RetrieveWithTiers
  participant L1Hits
  participant MemoryStore
  RetrieveV2->>RetrieveV2: Resolve supersession configuration
  RetrieveV2->>RetrieveWithTiers: Pass lane parameters
  RetrieveWithTiers->>L1Hits: Pass scoring parameters
  L1Hits->>MemoryStore: Query SUPERSEDES destinations
  MemoryStore-->>L1Hits: Return superseded belief IDs
  L1Hits-->>RetrieveWithTiers: Demote or exclude candidates
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: adding a default-off supersession lane that can demote or exclude beliefs.
Description check ✅ Passed The description covers the summary, linked issue, verification, and reviewer notes, with only some template sections omitted.
Linked Issues check ✅ Passed The PR implements the requested default-off supersession lane, env/kwarg/TOML precedence, batched lookup, and both demote/exclude paths for #1187.
Out of Scope Changes check ✅ Passed The added docs, changelog, and tests all support the supersession retrieval work; no unrelated code changes stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 fix/issue-1187-supersession-demote

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 30, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 960 changed lines (limit: 200)
  • 5 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 2 issues, and left some high level feedback:

  • In _l1_hits you recompute superseded_belief_ids separately on the BM25F and FTS5 paths; consider factoring this into a single helper or computing it once per call to avoid duplicated work on larger candidate sets.
  • The resolver functions (resolve_supersession_treatment, resolve_supersession_factor, _read_toml_str_for) emit diagnostics via print to stderr; it may be more consistent with the rest of the codebase to route these through a logging facility so callers can control verbosity.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_l1_hits` you recompute `superseded_belief_ids` separately on the BM25F and FTS5 paths; consider factoring this into a single helper or computing it once per call to avoid duplicated work on larger candidate sets.
- The resolver functions (`resolve_supersession_treatment`, `resolve_supersession_factor`, `_read_toml_str_for`) emit diagnostics via `print` to stderr; it may be more consistent with the rest of the codebase to route these through a logging facility so callers can control verbosity.

## Individual Comments

### Comment 1
<location path="tests/test_supersession_lane.py" line_range="369-378" />
<code_context>
+# --- Reachable from the production path ----------------------------------
+
+
+def test_retrieve_v2_threads_the_lane(
+    store: MemoryStore, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    """The lane has to be reachable from `retrieve_v2`, not just `_l1_hits`.
+
+    `uri_baki.apply_supersession_demote` had no importer for exactly this
+    reason — a primitive nothing calls is not a fix. Uses the exclusion arm
+    because it is unambiguous end-to-end (the demote arm composes with the
+    default-ON entity lane, per the composition test above).
+    """
+    monkeypatch.delenv(retrieval.ENV_SUPERSESSION_DEMOTE, raising=False)
+
+    off = retrieve_v2(store, "deploy target")
+    on = retrieve_v2(
+        store, "deploy target", use_supersession_demote=True,
+        supersession_treatment=SUPERSESSION_TREATMENT_EXCLUDE,
+    )
+
+    assert _order(off.beliefs) == ["superseded", "current"]
+    assert _order(on.beliefs) == ["current"]
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test that exercises `retrieve()` itself so the supersession lane is validated all the way through the default public API.

`test_retrieve_v2_threads_the_lane` verifies wiring through `retrieve_v2`, but most callers use `retrieve()`. Without a test for `retrieve()`, the lane could be dropped or mis-threaded there without detection. Please add a test that calls `retrieve()` with `use_supersession_demote=True` and `supersession_treatment=SUPERSESSION_TREATMENT_EXCLUDE`, and asserts the same belief ordering, to lock in this behavior across the default API surface.

Suggested implementation:

```python
    assert _order(off.beliefs) == ["superseded", "current"]
    assert _order(on.beliefs) == ["current"]


def test_retrieve_threads_the_lane(
    store: MemoryStore, monkeypatch: pytest.MonkeyPatch,
) -> None:
    """The supersession lane must also be threaded through `retrieve()`.

    Mirrors `test_retrieve_v2_threads_the_lane` but exercises the default
    public retrieval API, so that changes to `retrieve()` wiring cannot drop
    or mis-thread the supersession demote lane without test failures.
    """
    monkeypatch.delenv(retrieval.ENV_SUPERSESSION_DEMOTE, raising=False)

    off = retrieve(store, "deploy target")
    on = retrieve(
        store, "deploy target", use_supersession_demote=True,
        supersession_treatment=SUPERSESSION_TREATMENT_EXCLUDE,
    )

    assert _order(off.beliefs) == ["superseded", "current"]
    assert _order(on.beliefs) == ["current"]

```

1. Ensure `retrieve` is imported into `tests/test_supersession_lane.py` alongside `retrieve_v2` from the appropriate module (likely the same module where `retrieve_v2` is imported).
2. If `retrieve` returns a different shape than `retrieve_v2` (e.g., the beliefs are on `result.beliefs` vs. directly on the result), adjust `off`/`on` handling accordingly while keeping the assertions on the belief ordering identical.
</issue_to_address>

### Comment 2
<location path="tests/test_supersession_lane.py" line_range="305-311" />
<code_context>
+    assert is_supersession_demote_enabled(start=tmp_path) is True
+
+
+def test_treatment_defaults_to_the_safer_arm(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    """A wrong demote leaves a ranking signal; a wrong exclusion does not."""
+    monkeypatch.delenv(retrieval.ENV_SUPERSESSION_TREATMENT, raising=False)
+
+    assert resolve_supersession_treatment(start=tmp_path) == (
+        SUPERSESSION_TREATMENT_DEMOTE
+    )
</code_context>
<issue_to_address>
**suggestion (testing):** Precedence for supersession treatment/factor resolution is only partially tested; consider explicit precedence tests similar to the lane flag.

Current tests cover `is_supersession_demote_enabled` precedence and the normalisation/error paths of `resolve_supersession_treatment` and `resolve_supersession_factor`, but not their precedence behaviour when multiple sources are set. Please add parametrized tests that assert the documented order (env > kwarg > TOML > default) for both resolvers—for example, "env overrides kwarg", "kwarg overrides TOML"—so configuration resolution precedence is fully exercised and guarded against regressions.

Suggested implementation:

```python
def test_kwarg_beats_toml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv(retrieval.ENV_SUPERSESSION_DEMOTE, raising=False)
    (tmp_path / ".aelfrice.toml").write_text(
        "[retrieval]\nuse_supersession_demote = true\n", encoding="utf-8",
    )

    assert is_supersession_demote_enabled(False, start=tmp_path) is False
    assert is_supersession_demote_enabled(start=tmp_path) is True



from aelfrice.retrieval import (
    SUPERSESSION_DEMOTE_FACTOR,
    SUPERSESSION_TREATMENT_DEMOTE,
    SUPERSESSION_TREATMENT_EXCLUDE,
    _l1_hits,
    _supersession_penalty,
    is_supersession_demote_enabled,
    resolve_supersession_factor,
    resolve_supersession_treatment,
    retrieve_v2,
)


@pytest.mark.parametrize(
    "env_value, kwarg_value, toml_value, expected",
    [
        # env overrides kwarg and TOML
        (
            SUPERSESSION_TREATMENT_EXCLUDE,
            SUPERSESSION_TREATMENT_DEMOTE,
            SUPERSESSION_TREATMENT_DEMOTE,
            SUPERSESSION_TREATMENT_EXCLUDE,
        ),
        # kwarg overrides TOML when env is unset
        (
            None,
            SUPERSESSION_TREATMENT_EXCLUDE,
            SUPERSESSION_TREATMENT_DEMOTE,
            SUPERSESSION_TREATMENT_EXCLUDE,
        ),
        # TOML used when env and kwarg are unset
        (
            None,
            None,
            SUPERSESSION_TREATMENT_EXCLUDE,
            SUPERSESSION_TREATMENT_EXCLUDE,
        ),
        # default used when no sources are set
        (
            None,
            None,
            None,
            SUPERSESSION_TREATMENT_DEMOTE,
        ),
    ],
)
def test_resolve_supersession_treatment_precedence(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    env_value: str | None,
    kwarg_value: str | None,
    toml_value: str | None,
    expected: str,
) -> None:
    """Ensure env > kwarg > TOML > default precedence for supersession treatment."""
    if env_value is None:
        monkeypatch.delenv(retrieval.ENV_SUPERSESSION_TREATMENT, raising=False)
    else:
        monkeypatch.setenv(retrieval.ENV_SUPERSESSION_TREATMENT, env_value)

    if toml_value is not None:
        (tmp_path / ".aelfrice.toml").write_text(
            "[retrieval]\nsupersession_treatment = \""
            + toml_value
            + "\"\n",
            encoding="utf-8",
        )

    assert resolve_supersession_treatment(kwarg_value, start=tmp_path) == expected


@pytest.mark.parametrize(
    "env_value, kwarg_value, toml_value, expected",
    [
        # env overrides kwarg and TOML
        ("0.5", 0.1, 0.2, 0.5),
        # kwarg overrides TOML when env is unset
        (None, 0.3, 0.1, 0.3),
        # TOML used when env and kwarg are unset
        (None, None, 0.4, 0.4),
        # default used when no sources are set
        (None, None, None, SUPERSESSION_DEMOTE_FACTOR),
    ],
)
def test_resolve_supersession_factor_precedence(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    env_value: str | None,
    kwarg_value: float | None,
    toml_value: float | None,
    expected: float,
) -> None:
    """Ensure env > kwarg > TOML > default precedence for supersession demote factor."""
    if env_value is None:
        monkeypatch.delenv(retrieval.ENV_SUPERSESSION_DEMOTE_FACTOR, raising=False)
    else:
        monkeypatch.setenv(retrieval.ENV_SUPERSESSION_DEMOTE_FACTOR, env_value)

    if toml_value is not None:
        (tmp_path / ".aelfrice.toml").write_text(
            "[retrieval]\nsupersession_demote_factor = "
            + str(toml_value)
            + "\n",
            encoding="utf-8",
        )

    assert resolve_supersession_factor(kwarg_value, start=tmp_path) == expected

```

These changes assume:
1. `ENV_SUPERSESSION_TREATMENT` and `ENV_SUPERSESSION_DEMOTE_FACTOR` exist on the `aelfrice.retrieval` module and are the correct environment variable names; if they differ, update the references accordingly.
2. The TOML keys `supersession_treatment` and `supersession_demote_factor` match the actual configuration schema; if different, adjust the keys in the `.write_text(...)` calls.
3. `SUPERSESSION_TREATMENT_DEMOTE` and `SUPERSESSION_TREATMENT_EXCLUDE` are the string values expected by `resolve_supersession_treatment`. If the resolvers operate on enums or other types, adapt the parameter types and TOML/env string representations to match.
4. If type annotations such as `str | None` and `float | None` are inconsistent with the project's minimum Python version or style, you may need to switch to `Optional[str]` / `Optional[float]` or omit annotations to align with the rest of the test module.
</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_supersession_lane.py
Comment thread tests/test_supersession_lane.py
@robotrocketscience
robotrocketscience force-pushed the fix/issue-1187-supersession-demote branch 2 times, most recently from 28a959e to a3e2295 Compare July 30, 2026 04:27

@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/retrieval.py (1)

2966-3033: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring gap: new supersession params undocumented.

Every other _l1_hits flag (use_bm25f_anchors, gamma_temperature, zeta_params, heat_kernel_on, etc.) gets a dedicated paragraph in the docstring, but use_supersession_demote / supersession_treatment / supersession_factor (added at Lines 2980-2982) aren't mentioned there at all — the behavior is only documented via inline comments deeper in the function body. Worth a short paragraph for discoverability, matching this file's own documentation convention.

🤖 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/retrieval.py` around lines 2966 - 3033, Document the
supersession options in the _l1_hits docstring: describe
use_supersession_demote, supersession_treatment, and supersession_factor,
including their role in reranking and the available treatment behavior as
established by the function implementation. Match the concise
dedicated-paragraph style used for the other scoring flags, without changing
runtime logic.
🤖 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/retrieval.py`:
- Around line 2966-3033: Document the supersession options in the _l1_hits
docstring: describe use_supersession_demote, supersession_treatment, and
supersession_factor, including their role in reranking and the available
treatment behavior as established by the function implementation. Match the
concise dedicated-paragraph style used for the other scoring flags, without
changing runtime logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fa0b5bf6-2925-45ba-a025-a507abacf3b9

📥 Commits

Reviewing files that changed from the base of the PR and between d4fb753 and a3e2295.

📒 Files selected for processing (5)
  • CHANGELOG/v4.md
  • docs/user/CONFIG.md
  • src/aelfrice/retrieval.py
  • src/aelfrice/store.py
  • tests/test_supersession_lane.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/user/CONFIG.md
  • CHANGELOG/v4.md

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Setr:2026-07-30T17:00:35Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Not approving yet. The mechanism, the resolvers and finding 1 are all correct — and finding 1 is the right call, independently corroborated below. But the exclusion arm has a defect that would make the bench it exists to serve unreadable, and it should be fixed before the operator commissions that bench.


The blocker: exclusion shrinks the pack, it does not backfill

search_beliefs_scored(query, limit=l1_limit) applies the limit in SQL, and the exclusion filter runs on the rows that come back. Everything below the cutoff stays below it. So the exclusion arm doesn't return "the same pack minus retired beliefs" — it returns a smaller pack, with current, relevant beliefs stranded just under the limit.

Reproduced on this branch. Seven beliefs match deploy; the three strongest matches are superseded; l1_limit=4:

control   l1_limit=4 -> 4 returned
demote    l1_limit=4 -> 4 returned
exclude   l1_limit=4 -> 1 returned      <-- three non-superseded beliefs sat at ranks 5-7

One belief instead of four, with three perfectly good current beliefs available and unreached. In the degenerate case — every top-l1_limit candidate retired — the exclusion arm returns an empty L1 while the store holds the answer.

Two consequences, and the second is the one that matters for this PR's purpose:

  1. It is wrong on its own terms. "The user retired this claim" should mean the retired belief yields its slot to the next candidate, not that the slot evaporates. This is structurally the same failure the lock-budget starvation fix addressed (fix(retrieval): locked beliefs starve query-relevant retrieval when locks meet/exceed the token budget #1014/fix(retrieval): reserve a relevance budget floor so locks can't starve query-relevant hits (#1014) #1015) — a filter applied after the budget starves the pack.

  2. It confounds the three-arm bench. Demote and exclude would be measured with systematically different pack sizes, so the arms differ in two variables at once. If exclusion loses, nobody can say whether removing superseded content hurt or whether the smaller pack did. That is exactly the class of unreadable result this lane was split out to avoid.

The fix is contained: on the exclusion arm, over-fetch (limit=l1_limit + k, or widen and retry while short) and truncate to l1_limit after filtering. The demote arm needs no change — it reorders within a fixed candidate set, which is why it measures 4/4 above.

I'd rather this land in this PR than as a follow-up, because a bench run against the current exclusion arm produces a number that looks authoritative and isn't.


Finding 1 is right, and it is already established here

The multiplicative-inversion analysis is correct and matches a prior finding in this codebase: the composite rerank score is log-domain and routinely negative, so a multiplicative demote inverts into a promotion. uri_baki.apply_supersession_demote is written against a non-negative score scale that this rerank does not produce, and leaving it unimported with a test pinning the negative-score premise is the right disposition — better than importing it and "fixing" it into something whose name no longer describes it.

_supersession_penalty returning min(0.0, log(max(factor, EPS))) is sound: with factor clamped to (0, 1] the term is always ≤ 0, factor = 0 floors at log(1e-6) instead of -inf, and factor > 1 clamps rather than promoting.

Finding 2 is right, and the recommendation should be stronger

Agreed that log(0.5) = -0.69 is the same order as the entity-persistence penalty and roughly 10× weaker than its log(ε) = -6.91 floor, so composition can cancel it. Sweep the factor; do not bench 0.5 alone. A pinned-0.5 run that reports "demotion doesn't work" would be measuring the pin, not the mechanism.

Verified

  • superseded_belief_ids returns dst, i.e. the retired belief — the fix(graph): SUPERSEDES points new→old, so BFS surfaces the stale belief at maximum path score #1170 direction, not its inverse. The dedicated test for this is well placed; getting it backwards would reintroduce the inversion one layer down.
  • The batched query is indexed: idx_edges_dst exists, so the json_each join doesn't table-scan on the default retrieval path.
  • The json_each binding keeps the SQL text static, which also sidesteps the placeholder-IN false positive the security check-run raises.
  • Both short-circuits gained and not use_supersession_demote, so lane-off is byte-identical on both the BM25F and FTS5 paths — and the monkeypatch test that asserts the store helper is never called with the lane off is the right way to pin it.
  • Exclusion runs before bm25_pos_by_id / the heat-kernel seeds, so a retired belief doesn't seed the graph lane. Correct, and easy to get wrong.
  • Default-OFF confirmed through retrieve()retrieve_v2 → resolver.

Smaller notes

  1. _read_toml_str_for stops at the first .aelfrice.toml it finds and returns None if the key is absent, rather than continuing up the tree. That matches _read_toml_flag_for, so it's consistent — noting it only because "walk up until found" and "walk up until a config file exists" read the same at the call site and differ when a repo has nested configs.

  2. superseded_belief_ids doesn't check that the superseding belief is still alive. If the newer belief is deleted or retired, its dst stays demoted with nothing having replaced it — the store keeps hiding the old answer and offers no new one. Whether edge cascade makes this reachable is worth a look before the bench, since it would show up as unexplained recall loss.

  3. The xpass observation on test_promotion_adversarial.py C6-01…C6-04 is a good catch and worth its own issue — markers that hide a working guard are the same category of dead signal as the rest of [Umbrella] The measurement apparatus cannot detect the defects it exists to catch #1160.


Requested: the over-fetch fix on the exclusion arm. Everything else here I'd approve as-is. Re-ping me and I'll re-review promptly; the reproduction above is three beliefs and a loop if you want it as a test.

@robotrocketscience robotrocketscience added attn:unblock Needs answer from another session and removed attn:review Needs review (PR open, awaiting reviewer) labels Jul 30, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Setr:2026-07-30T17:04:04Z]

@robotrocketscience
robotrocketscience force-pushed the fix/issue-1187-supersession-demote branch from 6c9495f to ba067c4 Compare July 30, 2026 17:25
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Setr:2026-07-30T18:20:22Z]

@robotrocketscience
robotrocketscience force-pushed the fix/issue-1187-supersession-demote branch from ba067c4 to 6562ccb Compare July 30, 2026 18:24
robotrocketscience added a commit that referenced this pull request Jul 30, 2026
…king it

The candidate limit is applied by the search — SQL LIMIT on the FTS5
path, top_k on BM25F — so filtering superseded beliefs afterwards
dropped the pack size by however many were retired and left current,
relevant beliefs stranded just below the cutoff:

  l1_limit=4, three of the top four retired  ->  1 belief returned
  l1_limit=10, eight of the top ten retired  ->  2 beliefs returned

Same shape as the lock-budget starvation fixed in #1014/#1015: a filter
applied after the budget starves the pack. In the degenerate case every
top-l1_limit candidate is retired and the arm returned nothing while the
store held the answer.

The exclusion arm now widens the fetch and retries, stopping as soon as
it has l1_limit survivors or the search runs out of matches, bounded at
three rounds. The demote arm is untouched — it reorders a fixed
candidate set, which is why it always measured full.

This also matters for the ratified three-arm bench: with the arms
differing in pack size as well as in treatment, a loss for exclusion
could not have been attributed to either.

Raised in review on #1191.
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Rebased onto 4f5f866d and fixed the exclusion-arm starvation. Now 6562ccb9, FF on main, 5 signed commits, discretion clean.

Exclusion now backfills instead of shrinking. Measured on a 20-belief matching set with the strongest 8 retired:

                 before          after
l1_limit=4       0 returned      4 returned, 0 superseded
l1_limit=10      2 returned      10 returned, 0 superseded

The arm widens the candidate fetch and retries, stopping as soon as it has l1_limit survivors or the search runs out of matches — bounded at three rounds (limit, 2x, 4x) so a store where nearly everything is retired costs a fixed number of queries rather than a scan. The demote arm is untouched: it reorders a fixed candidate set, which is exactly why it always measured full.

On the BM25F path the refetch reuses the already-computed scored_pairs when top_k == l1_limit, so the common case costs no extra scoring pass.

Three tests, each load-bearing — removing the widening fails the backfill test:

  • backfill to l1_limit at two limits, asserting zero survivors are retired;
  • the widening loop terminates on an exhausted search rather than doubling forever (6 matching, 4 retired, l1_limit=10 → exactly 2);
  • a control that demote still keeps retired beliefs in the pack, so the two arms remain genuinely different treatments rather than two spellings of exclusion.

Checked the #1195 gate. That merged while this was under review and now binds any PR touching src/aelfrice/**: aelf eval --json is byte-identical to the pinned baseline on this branch. 385 retrieval-adjacent tests green.

The two findings in the PR body still stand and are still the right advice for the bench. The multiplicative-inversion analysis is correct, and the factor-0.5 weakness is real — sweep the factor, don't pin it. What changes is that a loss for exclusion is now attributable: previously the arms differed in pack size as well as in treatment, so the bench could not have separated 'removing retired content hurt' from 'the pack got smaller'.

Labelling once CI settles.

@robotrocketscience
robotrocketscience force-pushed the fix/issue-1187-supersession-demote branch from 6562ccb to 0ed7302 Compare July 30, 2026 18:30
robotrocketscience added a commit that referenced this pull request Jul 30, 2026
…king it

The candidate limit is applied by the search — SQL LIMIT on the FTS5
path, top_k on BM25F — so filtering superseded beliefs afterwards
dropped the pack size by however many were retired and left current,
relevant beliefs stranded just below the cutoff:

  l1_limit=4, three of the top four retired  ->  1 belief returned
  l1_limit=10, eight of the top ten retired  ->  2 beliefs returned

Same shape as the lock-budget starvation fixed in #1014/#1015: a filter
applied after the budget starves the pack. In the degenerate case every
top-l1_limit candidate is retired and the arm returned nothing while the
store held the answer.

The exclusion arm now widens the fetch and retries, stopping as soon as
it has l1_limit survivors or the search runs out of matches, bounded at
three rounds. The demote arm is untouched — it reorders a fixed
candidate set, which is why it always measured full.

This also matters for the ratified three-arm bench: with the arms
differing in pack size as well as in treatment, a loss for exclusion
could not have been attributed to either.

Raised in review on #1191.
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Both Sourcery threads were valid — added, and rebased onto current main. Now 0ed7302f, FF, 6 signed commits, calibration byte-identical, discretion clean.

retrieve() coverage. This one is worth more than a routine test request: "lane wired into retrieve_v2 only" is a defect class this repo has hit before — the production hook path goes through retrieve(), so a lane reachable from tests and the bench harness and from nothing a user runs looks shipped and isn't. Driven by the env override, which is the only handle retrieve() exposes for this.

I checked what it actually catches rather than assuming, and the docstring now says so: it fails if retrieve_v2 stops consulting the resolver or the lane is dropped from _l1_hits (both verified), but it does not fail if retrieve() starts passing a hardcoded False instead of None — the resolver reads env before kwarg, so that edit is unobservable from there by construction. The guarantee is end-to-end reachability, not the shape of one argument. Better to write that down than to let the next reader assume more.

Precedence. Added for both resolve_supersession_treatment and resolve_supersession_factor, asserting each layer while the layer below it disagrees, so a resolver reading a single source fails rather than coincides. Swapping env and kwarg fails it. Factor values chosen distinct and in range so a clamp cannot make two layers agree by accident.

Labelling once CI settles.

@robotrocketscience robotrocketscience removed the attn:unblock Needs answer from another session label Jul 30, 2026
robotrocketscience added a commit that referenced this pull request Jul 30, 2026
…king it

The candidate limit is applied by the search — SQL LIMIT on the FTS5
path, top_k on BM25F — so filtering superseded beliefs afterwards
dropped the pack size by however many were retired and left current,
relevant beliefs stranded just below the cutoff:

  l1_limit=4, three of the top four retired  ->  1 belief returned
  l1_limit=10, eight of the top ten retired  ->  2 beliefs returned

Same shape as the lock-budget starvation fixed in #1014/#1015: a filter
applied after the budget starves the pack. In the degenerate case every
top-l1_limit candidate is retired and the arm returned nothing while the
store held the answer.

The exclusion arm now widens the fetch and retries, stopping as soon as
it has l1_limit survivors or the search runs out of matches, bounded at
three rounds. The demote arm is untouched — it reorders a fixed
candidate set, which is why it always measured full.

This also matters for the ratified three-arm bench: with the arms
differing in pack size as well as in treatment, a loss for exclusion
could not have been attributed to either.

Raised in review on #1191.
@robotrocketscience
robotrocketscience force-pushed the fix/issue-1187-supersession-demote branch from 0ed7302 to 5e72138 Compare July 30, 2026 18:34
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 30, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Setr:2026-07-30T18:36:49Z]

@github-actions

Copy link
Copy Markdown

merge-train: blocked

branch is not fast-forward on main (branch base 4ecac50a19b5aefd600e8dc0d04ff5cd8f571d97, current main 954d85abe3820af6de622f826fc13d523459db08). 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 Jul 30, 2026
One query returning which of a candidate set a SUPERSEDES edge points at.
Direction is the producers' canonical one (#1170): src is the newer
belief, dst the one it retires, so a belief is superseded when it appears
as a dst.

The candidate set binds as a single JSON array read through json_each
rather than an interpolated IN list, following ca97776 — the SQL text
stays static and placeholder count cannot drift. Batched because the
caller runs it inside the L1 rerank.
Retrieval had no notion of supersession: grep SUPERSED over retrieval.py
returned nothing, and uri_baki.apply_supersession_demote had no importer.
So correcting "deploy target is heroku" to "fly.io" left the next prompt
injecting heroku first, ahead of its own replacement.

Ships both ratified arms behind one default-OFF use_supersession_demote
flag — demote by log(factor), or exclude from the candidate set before the
heat-kernel seeds are computed — selected by supersession_treatment and
resolved env > kwarg > TOML > default like every neighbouring lane. The
default stays off: unlike the #1170 BFS fix this changes retrieve() output
on the default path, so the three-arm bench picks the winner.

The demote is additive, which corrects the issue's suggested wiring. The
composite rerank score is log-domain and routinely negative (-13.08 on the
reproduction), so score * 0.5 raises it — importing the multiplicative
primitive would have promoted the superseded belief to the top, the exact
inversion this fixes. Adding log(factor) is the log-domain equivalent of
scaling a probability, so the factor semantics survive intact.
26 tests: edge direction (dst not src, so the #1170 inversion cannot
reappear one layer down), candidate scoping, other edge types ignored,
both arms through the L1 rerank, exclusion emptying the pack, and
reachability from retrieve_v2 — the gap that left the uri_baki primitive
importerless.

Two pin findings rather than behaviour. One asserts the negative-score
premise directly, so the additive penalty is not "simplified" back into a
multiplication. The other records the measured composition with the
default-ON entity-persistence lane: at factor 0.5 the two penalties are
the same order of magnitude and on this corpus they cancel, which is why
the bench needs to sweep the factor.
CONFIG.md gains the knob trio with the demote-vs-exclude trade-off, the
reason the penalty is additive, and the two measured calibration facts an
operator running the three-arm bench needs.
…king it

The candidate limit is applied by the search — SQL LIMIT on the FTS5
path, top_k on BM25F — so filtering superseded beliefs afterwards
dropped the pack size by however many were retired and left current,
relevant beliefs stranded just below the cutoff:

  l1_limit=4, three of the top four retired  ->  1 belief returned
  l1_limit=10, eight of the top ten retired  ->  2 beliefs returned

Same shape as the lock-budget starvation fixed in #1014/#1015: a filter
applied after the budget starves the pack. In the degenerate case every
top-l1_limit candidate is retired and the arm returned nothing while the
store held the answer.

The exclusion arm now widens the fetch and retries, stopping as soon as
it has l1_limit survivors or the search runs out of matches, bounded at
three rounds. The demote arm is untouched — it reorders a fixed
candidate set, which is why it always measured full.

This also matters for the ratified three-arm bench: with the arms
differing in pack size as well as in treatment, a loss for exclusion
could not have been attributed to either.

Raised in review on #1191.
…pin precedence

Two gaps Sourcery flagged, both real.

The lane was only exercised through retrieve_v2. Production goes through
retrieve(), and 'staged lane wired into retrieve_v2 only' is a defect
class this repo has hit before — a lane reachable from tests and the
bench harness and from nothing a user runs. The new test drives it by
env override, which is the only handle retrieve() exposes.

Treatment and factor resolution had normalisation and error-path tests
but no precedence tests. Each layer is now asserted while the layer
below it disagrees, so a resolver reading one source would fail rather
than coincide. Swapping env and kwarg fails it.

The retrieve() test's docstring states what it does not catch: a
hardcoded False in retrieve() is unobservable because the resolver reads
env before kwarg. It guards resolver consultation and _l1_hits wiring,
which is what neutering either does fail.
@robotrocketscience
robotrocketscience force-pushed the fix/issue-1187-supersession-demote branch from 5e72138 to fc6f736 Compare July 30, 2026 18:37
@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 fc6f736 into main Jul 30, 2026
27 of 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 fc6f736main via FF push.

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

Labels

author-Kulili PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(retrieval): superseded beliefs are never demoted or excluded — the demoter has no importer

1 participant