diff --git a/CHANGELOG/v3.md b/CHANGELOG/v3.md index 47cf6ab4f..895774795 100644 --- a/CHANGELOG/v3.md +++ b/CHANGELOG/v3.md @@ -38,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Entity-persistence demotion flipped default-on in `retrieve_v2` ([#1096](https://github.com/robotrocketscience/aelfrice/issues/1096)).** The lane added default-off earlier this cycle now defaults on, once the G2 mixed-corpus eval ([#1103](https://github.com/robotrocketscience/aelfrice/issues/1103)) cleared the no-regression gate: durable recall held (20→20), ephemeral coordination demoted (20→3 at the tight pack budget), MRR 0.883→1.000, and the lane is recall-safe / inert on LoCoMo (whose entities are all `noun_phrase`). The resolver now also honours a `[retrieval] use_entity_persist_demote` TOML rung (precedence env → kwarg → TOML → default), so a project can opt out without an env var — matching the `use_heat_kernel` idiom and making the `CONFIG.md` precedence accurate. **Scope:** the lane is wired into `retrieve_v2()` only — the live `UserPromptSubmit` hook uses the legacy `retrieve()` path, which does not expose it, so this flip governs `retrieve_v2` consumers (benchmarks, the eval suite, direct callers) and does **not** change production hook retrieval; the `retrieve()`/`retrieve_v2()` production-path gap is filed for disposition in [#1107](https://github.com/robotrocketscience/aelfrice/issues/1107). + - **Conversational capture is now a first-class `transcript` ingest source ([#1089](https://github.com/robotrocketscience/aelfrice/issues/1089), axis-2 prerequisite).** Passive/transcript capture previously recorded `source_kind=filesystem` in `ingest_log`, indistinguishable from project-file scanning; it now records a new `INGEST_SOURCE_TRANSCRIPT` kind. Behaviour is otherwise preserved — the retention default is unchanged (`RETENTION_FACT`), the `role=user → origin=user_transcript` undeflated-prior routing still fires, and replay-equality holds (a `transcript_v0_1.jsonl` replay-soak corpus covers the new kind, re-deriving on the `agent_inferred` path so it is stable under the `raw_meta=None` production replay). Side benefit: the user-transcript branch now keys on the transcript *source kind* rather than a bare `source_path=="transcript"` string match, so a project file literally named `transcript` can no longer spoof the undeflated user prior. This names the source so "rank curated claude-memory above conversational capture" (axis 2) becomes expressible; the ranking lever itself is a separate follow-up and this does **not** close #1089. ### Fixed diff --git a/docs/concepts/ARCHITECTURE.md b/docs/concepts/ARCHITECTURE.md index e93e34a1c..f7c2e7fef 100644 --- a/docs/concepts/ARCHITECTURE.md +++ b/docs/concepts/ARCHITECTURE.md @@ -122,12 +122,12 @@ Dedupe L1+L2.5+L3 against L0 ids Trim from tail until sum(estimated_tokens) ≤ token_budget ``` -Two **default-off rerank modifiers** refine the ranked (L1 / L2.5) tiers without adding a lane: +Two **rerank modifiers** refine the ranked (L1 / L2.5) tiers without adding a lane (both wired into `retrieve_v2` only — the legacy `retrieve()` hook path exposes neither): -- **Entity-persistence demotion** (#1096, `[retrieval] use_entity_persist_demote` / `AELFRICE_ENTITY_PERSIST_DEMOTE`) — the organic sink for #1086's junk-percolation problem. A log-additive **demotion** term `min(0, log(S1 + ε))`, where `S1 = durable / (durable + transient + 1)` is read from the `belief_entities` index (one batched query), down-weights candidates that ground only to *transient* coordination tokens (bare PR/issue numbers, version/branch tags) relative to *durable* entities (file paths, error codes, symbols). Pure demotion (well-grounded candidates are neutral, never boosted); applied only to entity-bearing candidates, so entity-free prose is untouched. Note the sink is **content-referential, not temporal** — a time/cold-decay sink was measured empirically inert here (the junk is *hot*, not stale). Default-ON flip is gated on a retrieval-bench no-regression check and is a separate operator call. +- **Entity-persistence demotion** (#1096, `[retrieval] use_entity_persist_demote` / `AELFRICE_ENTITY_PERSIST_DEMOTE`) — the organic sink for #1086's junk-percolation problem. A log-additive **demotion** term `min(0, log(S1 + ε))`, where `S1 = durable / (durable + transient + 1)` is read from the `belief_entities` index (one batched query), down-weights candidates that ground only to *transient* coordination tokens (bare PR/issue numbers, version/branch tags) relative to *durable* entities (file paths, error codes, symbols). Pure demotion (well-grounded candidates are neutral, never boosted); applied only to entity-bearing candidates, so entity-free prose is untouched. Note the sink is **content-referential, not temporal** — a time/cold-decay sink was measured empirically inert here (the junk is *hot*, not stale). **Default-ON in `retrieve_v2` since v4.0**, flipped once the #1096 G2 mixed-corpus eval (#1103) cleared the no-regression gate; opt out with a falsy env/kwarg/TOML rung. - **Origin-priority tie-break** (#1089, `[retrieval] use_origin_tiebreak` / `AELFRICE_ORIGIN_TIEBREAK`) — a within-tier tie-break (not a rerank term): when two candidates tie on relevance, the higher-trust *origin* wins, sitting between the relevance score and the id tie-break so relevance always dominates. Byte-identical when off. -Both are deterministic (#605) and byte-identical to the prior pipeline when their flags are unset. +Both are deterministic (#605) and byte-identical to the prior pipeline when their flags resolve falsy (origin tie-break is default-off; entity-persistence demotion is default-on, so opt out explicitly for parity). Token estimate: `(len(content) + 3) // 4`. Empty query: L0 only. L0 always wins overflow. diff --git a/docs/user/CONFIG.md b/docs/user/CONFIG.md index 84a8fa8aa..21df75871 100644 --- a/docs/user/CONFIG.md +++ b/docs/user/CONFIG.md @@ -9,7 +9,7 @@ This is the reference for power users whose project has a documentation idiom or A single optional TOML file at the root of a project (or any ancestor). It exposes the following power-user surfaces: - `[noise]` — onboard-time belief filter. Changes how `aelf onboard` ingests beliefs; nothing else. -- `[retrieval]` (v1.3+) — retrieval-time tier toggles + ranking. Knobs: `entity_index_enabled` (L2.5), `bfs_enabled` (L3), `posterior_weight` (partial Bayesian-weighted L1 ranking), `l1_limit` + `token_budget` (the #1045 wide-retrieval knobs — BM25 candidate cap + token budget, default 50/2400; raise both together for multi-hop recall), `use_bm25f_anchors` (BM25F-with-anchor-text since v1.7), `use_heat_kernel` (authority scoring lane, default-on since v2.1), `use_hrr_structural` (HRR structural-query lane, default-on since v2.1), `hrr_persist` (HRR structural-index on-disk persistence, default-on since v3.0), `use_type_aware_compression` (per-belief retention-class compression, default-on since #769), `use_intentional_clustering` (co-locating related beliefs, default-on since v3.0), `expansion_gate_enabled`, `use_gamma_posterior_temperature` (default off), and `use_zeta_posterior_rerank` (default off; mutually exclusive with the γ flag — `retrieve()` raises `ValueError` when both are on), `use_temporal_spine` + `temporal_spine_budget` (the #1064 chronological-adjacency lane, default off/32; pairs with `[ingest] write_temporal_spine`), `use_entity_persist_demote` (the #1096 entity-persistence demotion / organic-sink rerank modifier, default off), `use_origin_tiebreak` (the #1089 origin-priority within-tier tie-break, default off). Two placeholder flags (`use_signed_laplacian`, `use_posterior_ranking`) are recognised but emit a deprecation warning if set — their lanes have not yet shipped. +- `[retrieval]` (v1.3+) — retrieval-time tier toggles + ranking. Knobs: `entity_index_enabled` (L2.5), `bfs_enabled` (L3), `posterior_weight` (partial Bayesian-weighted L1 ranking), `l1_limit` + `token_budget` (the #1045 wide-retrieval knobs — BM25 candidate cap + token budget, default 50/2400; raise both together for multi-hop recall), `use_bm25f_anchors` (BM25F-with-anchor-text since v1.7), `use_heat_kernel` (authority scoring lane, default-on since v2.1), `use_hrr_structural` (HRR structural-query lane, default-on since v2.1), `hrr_persist` (HRR structural-index on-disk persistence, default-on since v3.0), `use_type_aware_compression` (per-belief retention-class compression, default-on since #769), `use_intentional_clustering` (co-locating related beliefs, default-on since v3.0), `expansion_gate_enabled`, `use_gamma_posterior_temperature` (default off), and `use_zeta_posterior_rerank` (default off; mutually exclusive with the γ flag — `retrieve()` raises `ValueError` when both are on), `use_temporal_spine` + `temporal_spine_budget` (the #1064 chronological-adjacency lane, default off/32; pairs with `[ingest] write_temporal_spine`), `use_entity_persist_demote` (the #1096 entity-persistence demotion / organic-sink rerank modifier, default **on** in `retrieve_v2` since v4.0; not exposed on the legacy `retrieve()` path), `use_origin_tiebreak` (the #1089 origin-priority within-tier tie-break, default off). Two placeholder flags (`use_signed_laplacian`, `use_posterior_ranking`) are recognised but emit a deprecation warning if set — their lanes have not yet shipped. - `[rebuilder]` (v1.4+) — context-rebuilder knobs: `turn_window_n` (default 50), `token_budget` (default 4000), `trigger_mode` (`manual`|`threshold`|`dynamic`, default `threshold`), `threshold_fraction` (default 0.6), and `query_strategy` (v1.7+, default `stack-r1-r3` since v3.0). `[rebuild_floor]` (v1.7+) sets the token-budget floors for the session-scoped and L1 belief lanes (`[rebuild_floor] session` and `[rebuild_floor] l1`). - `[onboard.llm]` (v1.3.0+) — direct-API onboard classifier gate; documented under [Keys § `[onboard.llm]`](#onboardllm-v130) below. - `[cadence]`, `[implicit_feedback]`, and `[hook_audit]` — feedback-cadence scoring, deferred retrieval-exposure feedback, and the per-turn hook audit log. Recognised here but documented in their module docstrings (`src/aelfrice/cadence.py`, `src/aelfrice/deferred_feedback.py`, `src/aelfrice/hook.py`). @@ -396,13 +396,13 @@ Precedence (first decisive wins): env var `AELFRICE_POSTERIOR_WEIGHT=` > ### `use_entity_persist_demote` -Boolean, default `false` (v4.0+, [#1096](https://github.com/robotrocketscience/aelfrice/issues/1096)). Enables the **entity-persistence demotion lane** — a deterministic *organic sink* for the #1086 junk-percolation problem (junk ranks up, not down), applied as a log-additive rerank modifier over the ranked candidate tiers. +Boolean, default `true` in `retrieve_v2` since v4.0 ([#1096](https://github.com/robotrocketscience/aelfrice/issues/1096); flipped from default-off once the G2 mixed-corpus eval [#1103] cleared the no-regression gate). The **entity-persistence demotion lane** is a deterministic *organic sink* for the #1086 junk-percolation problem (junk ranks up, not down), applied as a log-additive rerank modifier over the ranked candidate tiers. **Scope:** the lane is wired into `retrieve_v2()` only — the legacy `retrieve()` path used by the live `UserPromptSubmit` hook does not expose it, so this default governs `retrieve_v2` consumers (benchmarks, the eval suite, direct callers) until the production cutover. For each entity-bearing candidate it reads a grounding score `S1 = durable / (durable + transient + 1)` from the `belief_entities` index (one batched query over the candidate set), then applies the penalty `min(0, log(S1 + ε))`. Beliefs that ground only to *transient* coordination tokens (bare PR/issue numbers, version/branch tags) are demoted below those that ground to *durable* entities (file paths, error codes, symbol identifiers). It is a **pure demotion** — well-grounded beliefs are neutral, never boosted — and touches only entity-bearing candidates, so entity-free durable content (docstrings, formulae) is never penalised. Measured separation on a 118-belief hand-labelled set: durable vs ephemeral S1 mean 0.56 vs 0.06, lifting the durable-above-ephemeral ranking AUC from 0.48 to 0.87. The sink is **content-referential, not temporal**: a time/recency decay sink was measured empirically inert for this workload (the junk is *hot*, not stale), so this lane — not cold-hibernation — is the organic sink. Deterministic per #605 (an entity-index join, no embeddings), byte-identical when unset. -Precedence (first decisive wins): env var `AELFRICE_ENTITY_PERSIST_DEMOTE=1` > explicit Python kwarg `use_entity_persist_demote=` on `retrieve()` / `retrieve_v2()` > TOML `[retrieval] use_entity_persist_demote` > default `false`. The default-ON flip is gated on a retrieval-bench no-regression check and is a separate operator call. +Precedence (first decisive wins): env var `AELFRICE_ENTITY_PERSIST_DEMOTE=1`/`0` > explicit Python kwarg `use_entity_persist_demote=` on `retrieve_v2()` > TOML `[retrieval] use_entity_persist_demote` > default `true`. Set any rung falsy to opt out for parity with the pre-flip ranking. ### `use_origin_tiebreak` @@ -410,7 +410,7 @@ Boolean, default `false` (v4.0+, [#1089](https://github.com/robotrocketscience/a This is a within-tier **tie-break**, never a primary rerank term — the origin key sits *between* the relevance score and the id tie-break, so relevance always dominates and byte-identical behaviour is preserved when the flag is off. It applies in both ranked tiers (the L1 FTS rerank and the L2.5 entity-index overlap). Deliberately *not* an origin *rerank lane* — that was refuted on LoCoMo in #1013 (the failure there was a BM25 recall limit, which reranking cannot fix). Deterministic per #605. -Precedence (first decisive wins): env var `AELFRICE_ORIGIN_TIEBREAK=1` > explicit Python kwarg `use_origin_tiebreak=` on `retrieve()` / `retrieve_v2()` > TOML `[retrieval] use_origin_tiebreak` > default `false`. Note a single-provenance corpus (e.g. LoCoMo) shares one origin tier, so the tie-break is inert there; the default-ON flip is a separate operator call. +Precedence (first decisive wins): env var `AELFRICE_ORIGIN_TIEBREAK=1` > explicit Python kwarg `use_origin_tiebreak=` on `retrieve_v2()` (the lane is not exposed on the legacy `retrieve()` path) > TOML `[retrieval] use_origin_tiebreak` > default `false`. Note a single-provenance corpus (e.g. LoCoMo) shares one origin tier, so the tie-break is inert there; the default-ON flip is a separate operator call. Negative values clamp to `0.0`. Non-numeric env values trace to stderr and fall through. The cache key is extended with the resolved weight (rounded to four decimals), so two callers passing different weights against the same store do not collide on a shared `RetrievalCache`. diff --git a/docs/user/LIMITATIONS.md b/docs/user/LIMITATIONS.md index c2dacc5b6..534458e7c 100644 --- a/docs/user/LIMITATIONS.md +++ b/docs/user/LIMITATIONS.md @@ -39,7 +39,7 @@ The default retrieval mode (recall, not audit) is correctly served by latest-ser - **Contradiction detection is partial.** Contradiction **resolution** (`aelf resolve`, the tie-breaker that picks a winner given a `CONTRADICTS` edge) ships; contradiction **detection** (the post-insert semantic-divergence detector that creates those edges automatically) is deferred. Until detection lands, contradiction-flagging surfaces only on edges produced by the triple extractor's six explicit relation-family regexes. Plus the `/aelf:reason` `VERDICT=CONTRADICTORY` path emits structured impasses at query time — but only over persisted `CONTRADICTS` edges. The edge-free query-time surface is the opt-in `AELF_SHOW_CONFLICTS=1` slot-conflict flag on `aelf search` (#938, v3.5.0), which marks hits whose value-slots collide with a locked belief. - **Natural-language sentiment is not captured automatically.** Saying "ok good" or "no that's wrong" in chat does **not** strengthen or weaken the beliefs the agent just used. Explicit `aelf feedback used|harmful` (CLI) or the equivalent MCP `aelf:feedback` call remains the high-confidence channel. Retrieval **exposure** is not a posterior signal by default: since #1086 (v4.0) surfacing a belief is audit-only (`AELFRICE_EXPOSURE_UPDATES_POSTERIOR`, default off), logged for the recurrence axis but leaving α/β untouched — a retrieval is exposure, not endorsement. The one residual exposure-as-evidence path is the v1.6 (#191) deferred-feedback sweeper: retrieval still enqueues each surfaced belief, and the *manual* `aelf sweep-feedback` pass applies a small alpha bump (default +0.05) after a grace window (default 30 minutes) unless an explicit signal arrived first (#1091 flagged this sweep for the same audit-only treatment). That is an exposure-based signal, not a sentiment-from-prose signal: the user's affect words are not parsed — unless you opt in to the sentiment-from-prose detector (`[feedback] sentiment_from_prose = true` in `.aelfrice.toml`, default off, #193/#606), which regex-matches short affect phrases and distributes the signal across the previous turn's retrieved beliefs. - **Confidence drops below 0.5 do not auto-flag.** A belief whose posterior drifts under the prior is not surfaced as a warning. No automatic state change is driven by negative evidence in v3.x — the v2.x demotion-pressure auto-demote was removed at [#814](https://github.com/robotrocketscience/aelfrice/issues/814). To find drifting beliefs, use `aelf speculative --json` (emits per-belief α/β for non-locked beliefs) or query the SQLite store directly (e.g. `SELECT id, alpha, beta FROM beliefs WHERE alpha / (alpha + beta) < 0.5`). -- **The organic sink is a default-off rerank modifier, not a posterior decay.** #1086 established that with exposure now audit-only, the store still lacks an automatic force that pushes low-value "coordination junk" (bare PR/issue numbers, version/branch chatter) *down*. The answer that shipped is the entity-persistence demotion lane (#1096, `[retrieval] use_entity_persist_demote`, **default off**): a deterministic rank down-weight that sinks candidates grounding only to transient tokens below those grounding to durable entities (file paths, error codes, symbols). It is content-referential, not time-based — a temporal/cold-decay sink was measured empirically inert (the junk is *hot*, not stale). It ranks, it does not delete; the default-ON flip is a separate operator call gated on a retrieval-bench no-regression check. To *find* junk for manual curation before then, use `aelf introspect --only-noise`. +- **The organic sink is a rerank modifier, not a posterior decay — and it is not yet on the live retrieval path.** #1086 established that with exposure now audit-only, the store still lacks an automatic force that pushes low-value "coordination junk" (bare PR/issue numbers, version/branch chatter) *down*. The answer that shipped is the entity-persistence demotion lane (#1096, `[retrieval] use_entity_persist_demote`): a deterministic rank down-weight that sinks candidates grounding only to transient tokens below those grounding to durable entities (file paths, error codes, symbols). It is content-referential, not time-based — a temporal/cold-decay sink was measured empirically inert (the junk is *hot*, not stale). It ranks, it does not delete. The flag **flipped default-on at v4.0 (#1103 cleared the G2 gate)**, but the lane is wired into `retrieve_v2` only — the live `UserPromptSubmit` hook still uses the legacy `retrieve()` path, which does not expose it, so the sink does not yet affect production retrieval (cutover tracked separately). To *find* junk for manual curation meanwhile, use `aelf introspect --only-noise`. - **Origin trust is a default-off tie-break only.** The origin-priority tie-break (#1089, `[retrieval] use_origin_tiebreak`, **default off**) lets a higher-trust origin win *ties* on relevance, but it is not a rerank term and never overrides relevance — an origin *rerank lane* was refuted on LoCoMo (#1013), where the real gap was BM25 recall, which reranking cannot fix. - **Decided-vs-floated lifecycle is not tracked automatically.** `aelf introspect` reports a floated-vs-decided *status* read off existing `RESOLVES` / `POTENTIALLY_STALE` edges, but there is no ingest-time producer that detects when a conversationally "floated" option later becomes a decision and promotes it (fix #4 of the #1086 scoring umbrella). It was measured non-retrofittable (no genuine float→decision pairs exist on real stores to seed from) and, after the #1081/#1083 hedge-drop discards float sentences at capture, non-applicable forward — so it was de-scoped ([#1100](https://github.com/robotrocketscience/aelfrice/issues/1100), closed). - **The decay target reads as 0.5.** Fresh beliefs start at type- and source-adjusted priors (0.375–0.95), not 0.5. The Jeffreys prior `(0.5, 0.5)` is the decay *target*: a belief whose evidence has fully decayed reads exactly `0.5`, which means "no surviving evidence," not "coin-flip true." diff --git a/src/aelfrice/retrieval.py b/src/aelfrice/retrieval.py index da79934c1..088a2177b 100644 --- a/src/aelfrice/retrieval.py +++ b/src/aelfrice/retrieval.py @@ -740,18 +740,34 @@ def _env_entity_persist_demote_override() -> bool | None: return None -def is_entity_persist_demote_enabled(kwarg: bool | None = None) -> bool: +def is_entity_persist_demote_enabled( + kwarg: bool | None = None, *, start: Path | None = None +) -> bool: """Resolve the entity-persistence demotion flag (#1096). - Precedence: AELFRICE_ENTITY_PERSIST_DEMOTE env var → explicit kwarg → - default False. Mirrors the HRR-expand resolution; default-OFF until - the bench-gated flip (#1096 G2).""" + Precedence (first decisive wins): + 1. AELFRICE_ENTITY_PERSIST_DEMOTE env var (truthy / falsy normalised). + 2. Explicit `kwarg` from the caller. + 3. `[retrieval] use_entity_persist_demote` in `.aelfrice.toml`. + 4. Default: **True** — the demotion lane flipped default-ON at v4.0 + once the #1096 G2 mixed-corpus eval (#1103) cleared the + no-regression gate: durable recall held (20→20), ephemeral + coordination demoted (20→3 at the tight pack budget), MRR + 0.883→1.000; recall-safe / inert on LoCoMo (all `noun_phrase`). + Opt out via the env var, kwarg, or TOML key for parity with the + pre-flip ranking. + + Reachable only from `retrieve_v2()`; the legacy `retrieve()` path does + not expose the lane (production cutover tracked separately).""" env = _env_entity_persist_demote_override() if env is not None: return env if kwarg is not None: return kwarg - return False + toml_value = _read_toml_flag_for(ENTITY_PERSIST_DEMOTE_FLAG, start) + if toml_value is not None: + return toml_value + return True def _env_origin_tiebreak_override() -> bool | None: diff --git a/tests/bench_gate/test_entity_persist_g2_mixed_corpus.py b/tests/bench_gate/test_entity_persist_g2_mixed_corpus.py index 78d733576..4c4b6641b 100644 --- a/tests/bench_gate/test_entity_persist_g2_mixed_corpus.py +++ b/tests/bench_gate/test_entity_persist_g2_mixed_corpus.py @@ -10,6 +10,12 @@ needs, built as a self-contained deterministic fixture so G2 is a repeatable public gate rather than a one-off lab measurement. +This eval cleared G2, and the lane subsequently flipped **default-ON in +``retrieve_v2``** at v4.0 (the resolver default; the legacy ``retrieve()`` +path does not expose the lane — production cutover is tracked +separately). The tests below pass the flag explicitly, so they pin +behaviour on both sides of the default and are unaffected by the flip. + The corpus (``_entity_persist_mixed_store``) pairs, per topic, durable technical beliefs (grounding to ``file_path`` / ``identifier`` / ``error_code``) with ephemeral coordination beliefs (grounding to @@ -163,16 +169,28 @@ def test_g2_recall_safe_and_demotes(corpus) -> None: ) -def test_g1_byte_identical_when_off(corpus) -> None: - """G1 companion: with the lane off, results are identical to a run - that never passes the flag (byte-identical default-off posture).""" +def test_default_on_after_flip(corpus) -> None: + """Post-#1096 flip: the lane is default-ON in ``retrieve_v2``. A run + that passes no flag matches an explicit ``demote=True`` run, and the + opt-out (``demote=False``) stays reachable and reorders at least one + topic — so the default flip is actually exercised, not a no-op. (G1's + byte-identical-when-off invariant itself is pinned by the explicit + ``demote=False`` arm in ``test_g2_recall_safe_and_demotes``.)""" store = corpus.store + saw_difference = False for topic in TOPICS: + explicit_on = _ids(store, topic.query, 1000, demote=True) explicit_off = _ids(store, topic.query, 1000, demote=False) default = [ b.id for b in retrieve_v2(store, topic.query, budget=1000).beliefs ] - assert explicit_off == default, ( - f"default != explicit-off for {topic.query!r}" + assert default == explicit_on, ( + f"default should equal explicit-ON (flip landed) for {topic.query!r}" ) + if explicit_off != default: + saw_difference = True + assert saw_difference, ( + "the lane must reorder at least one topic vs the off-path — " + "otherwise the default flip is untested" + ) diff --git a/tests/test_entity_persist_demote.py b/tests/test_entity_persist_demote.py index 78f96087d..34e4d6c3c 100644 --- a/tests/test_entity_persist_demote.py +++ b/tests/test_entity_persist_demote.py @@ -204,11 +204,16 @@ def test_penalty_monotone_low_s1_more_negative() -> None: # --- resolver ------------------------------------------------------------- -def test_resolver_default_off() -> None: - assert is_entity_persist_demote_enabled(None) is False +def test_resolver_default_on(monkeypatch: pytest.MonkeyPatch) -> None: + # v4.0 flip (#1096/#1103): with no env/kwarg/TOML rung set, the lane + # now resolves ON. + monkeypatch.delenv("AELFRICE_ENTITY_PERSIST_DEMOTE", raising=False) + assert is_entity_persist_demote_enabled(None) is True -def test_resolver_kwarg_true() -> None: +def test_resolver_kwarg_false_opts_out(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AELFRICE_ENTITY_PERSIST_DEMOTE", raising=False) + assert is_entity_persist_demote_enabled(False) is False assert is_entity_persist_demote_enabled(True) is True @@ -219,6 +224,20 @@ def test_resolver_env_overrides_kwarg(monkeypatch: pytest.MonkeyPatch) -> None: assert is_entity_persist_demote_enabled(False) is True +def test_resolver_toml_opts_out( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + # TOML rung (env > kwarg > TOML > default): a project that pins the + # flag false opts out even after the v4.0 default-on flip. + monkeypatch.delenv("AELFRICE_ENTITY_PERSIST_DEMOTE", raising=False) + (tmp_path / ".aelfrice.toml").write_text( + "[retrieval]\nuse_entity_persist_demote = false\n" + ) + assert is_entity_persist_demote_enabled(None, start=tmp_path) is False + # An explicit env/kwarg still wins over the TOML rung. + assert is_entity_persist_demote_enabled(True, start=tmp_path) is True + + # --- integration: retrieve_v2 + _l1_hits ---------------------------------- @@ -236,18 +255,21 @@ def _seed_two(store: MemoryStore) -> None: store._conn.commit() -def test_retrieve_v2_off_is_byte_identical_default() -> None: +def test_retrieve_v2_default_is_on() -> None: + # v4.0 flip (#1096): the no-flag default now matches explicit + # demote=True (the opt-out demote=False path is exercised by + # test_retrieve_v2_on_reorders_not_drops below). s = MemoryStore(":memory:") try: _seed_two(s) base = [b.id for b in retrieve_v2(s, "widget").beliefs] - off = [ + on = [ b.id - for b in retrieve_v2(s, "widget", use_entity_persist_demote=False).beliefs + for b in retrieve_v2(s, "widget", use_entity_persist_demote=True).beliefs ] finally: s.close() - assert base == off # default == explicit-off + assert base == on # default == explicit-on (flip landed) def test_retrieve_v2_on_reorders_not_drops() -> None: