Skip to content

feat(lock-consistency): annotation slots and lock-conflict mapping (#1175) - #1244

Merged
github-actions[bot] merged 3 commits into
mainfrom
feat/issue-1175-lock-consistency-annotate
Jul 31, 2026
Merged

feat(lock-consistency): annotation slots and lock-conflict mapping (#1175)#1244
github-actions[bot] merged 3 commits into
mainfrom
feat/issue-1175-lock-consistency-annotate

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Builds the library half of #1175's build-first item, greenlit by the operator
today after its stated blocker (#1227) closed via #1228. ANNOTATE only
DROP is not being built. Retrieval wiring and the hook.py render land
separately, matching how clustering.py shipped its pure half first.

Why this item

The L0 locked tier is materialised and injected ahead of everything else, and
nothing checks the L1/L2.5 tiers against it. So the model reads the user's most
explicit epistemic act and then reads a contradiction of it in the same prompt.

The suppression is the design, and two attempts at it were wrong

This issue said to design the noise suppression up front because 45% of
conflicts traced to one version-string lock. Measured across 189 real
prompts
replayed against the live 44,584-belief store, counting conflicts on
retrieved unlocked beliefs — the population that matters, per the earlier
R&D round, not the whole store.

Baseline 553 / 9,036 = 6.12%, with 6d849282 ("aelfrice v3.6.0 shipped
2026-06-19 …"
) alone at 63.7%.

Attempt 1 — cap the share of one pack a single lock may flag. Refuted. That
lock flags a p50 of 2.9% of any one pack (p90 = 10.2%). Its noise is spread
across queries, not concentrated within one, so a per-retrieval threshold
is structurally blind to it.

Attempt 2 — drop keys that are multi-valued within one belief. Helps, then
backfires.
The dominant lock carries key v with four distinct values in one
sentence (3.6, 3.5, 1.0, 3.7), which cannot be a functional dependency — a
principled rule needing no vocabulary. It takes 6.12% → 4.21% and evicts that
lock from the top five. But concentration got worse: the next
version-and-date lock took over at 72.4%. Multi-valuedness was a symptom.

What the extractor is actually doing, from that lock's slots:

NumericSlot(key='v',       value=3.6)            # version component
NumericSlot(key='shipped', value=2026.0)         # calendar year
NumericSlot(key='b88fd4',  value=9.0)            # a SHA fragment as a KEY
NumericSlot(key='run',     value=27806055357.0)  # a CI run id

Shipped rule — all three sources:

conflicts rate top-lock share
baseline 553 6.12% 63.7%
multi-valued only 380 4.21% 72.4% (worse)
all three 125 1.38% 27.2%

The residual spreads 27.2 / 16.8 / 14.4 / 12.8 / 11.2% across five locks —
roughly one annotated belief per 72 injected. That is the difference between an
annotation a user reads and one they learn to ignore.

Design decisions worth flagging

The filter lives here, not in value_compare.extract_values. Narrowing the
shipped extractor changes what counts as a numeric slot for every consumer —
the wider blast radius #1228 explicitly declined to take. The annotation gets
its own filtered view; the extractor is untouched.

Suppression is symmetric. An ambiguous key is exactly as meaningless on the
candidate side as on the lock side.

Suppression is slot-scoped, not belief-scoped. A belief carrying both a
version literal and a genuine disagreement is still annotated on the genuine
one.

Verification

Six mutations, all caught: removing each of the three rules independently,
widening the calendar band to every integer, and skipping candidate-side
suppression.

That last one escaped the first pass — it passed all 23 tests, because
every other case suppresses on the lock side first and so cannot see the
asymmetry. Found by running the mutation, not by reading the tests; there is
now a test for it, and the mutation fails that test and nothing else.

The controls are deliberate: a genuine conflict is annotated (without it,
over-broad suppression would satisfy every suppression test), a repeated
identical value is not treated as multi-valued, and values outside the
calendar band survive.

Full suite: 6701 passed, 69 skipped, 71 xfailed.

Not in this PR

Wiring into retrieve_with_tiers and the <belief … conflicts-with="…">
render at hook.py, behind a default-off flag. Threading the annotation map
out of the tier walk touches the result shape, so it is its own reviewable
change — and this module can be read and benched without a hot-path edit.

Summary by Sourcery

Introduce a library module to compute lock-consistency annotations between locked and unlocked beliefs, without changing retrieval wiring or drop behaviour.

New Features:

  • Add lock_consistency module that identifies slot-level conflicts between user locks and candidate beliefs for annotation purposes only.

Enhancements:

  • Define targeted suppression rules for noisy numeric slots (multi-valued keys, version-related keys, and calendar-year literals) to reduce spurious lock conflict annotations while keeping real disagreements.
  • Keep value extraction unchanged and make the annotation logic pure, deterministic, and driven by local predicates over belief slots.

Documentation:

  • Document the lock-consistency annotation behaviour and suppression design in both the new module and the v4 changelog entry.

Tests:

  • Add a dedicated test suite that pins each suppression rule, verifies symmetric and slot-scoped conflict detection, and ensures deterministic, repeatable annotation mapping behaviour.

…1175)

Pure half of the build-first item: which retrieved beliefs slot-conflict
with a user lock, with the noise suppression that makes the annotation
readable.

Suppression was measured, not asserted. Unsuppressed, 6.12% of retrieved
unlocked beliefs conflict and 63.7% of those trace to one version-and-date
lock. Two candidate rules were tried and rejected first: a per-retrieval
share cap (refuted -- that lock flags a p50 of 2.9% of any one pack, so its
noise is across queries not within one), and dropping multi-valued keys
alone (helps to 4.21% but concentration got worse, 72.4% on the next
version lock). Adding version-shaped keys and calendar-shaped values takes
it to 1.38% with the top lock at 27.2%, spread across five locks.

The filter lives here rather than in value_compare.extract_values:
narrowing the shipped extractor would change what counts as a numeric slot
for every consumer, which is the blast radius #1228 declined to take.

Not yet wired into retrieval.
…de too (#1175)

Passing the candidate's raw slots through instead of annotation_slots()
passed all 23 tests -- every other case is suppressed on the lock side
first, so the asymmetry was invisible to them. An ambiguous key is exactly
as meaningless on the candidate side, and now a test says so.
@robotrocketscience robotrocketscience added the author-Kulili PR coordination mutex label Jul 31, 2026

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

Sorry @robotrocketscience, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 31, 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: 22 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: 99765cf8-09f3-4a02-a2ee-1be229db6feb

📥 Commits

Reviewing files that changed from the base of the PR and between 54086da and 00b5eae.

📒 Files selected for processing (3)
  • CHANGELOG/v4.md
  • src/aelfrice/lock_consistency.py
  • tests/test_lock_consistency_1175.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.

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label Jul 31, 2026
@github-actions

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 364 changed lines (limit: 200)
  • 3 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 commented Jul 31, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements the library-side lock-consistency annotation for belief slots, including suppression rules for noisy numeric keys and a conflict-mapping helper, and pins the behavior with focused tests plus a changelog entry. Retrieval wiring and hook rendering are explicitly deferred.

Sequence diagram for lock_conflict_annotations flow

sequenceDiagram
    participant Caller
    participant lock_consistency
    participant value_compare

    Caller->>lock_consistency: lock_conflict_annotations(candidates, locked_pairs)
    alt locked_pairs empty
        lock_consistency-->>Caller: {}
    else locked_pairs non_empty
        lock_consistency->>lock_consistency: annotation_slots(slots) on each locked_pairs item
        loop candidates
            lock_consistency->>lock_consistency: annotation_slots(slots)
            alt candidate_slots non_empty
                loop filtered_locks
                    lock_consistency->>value_compare: find_conflicts(candidate_slots, lock_slots)
                    alt conflict_found
                        lock_consistency->>lock_consistency: out[belief_id] = lock.id
                        lock_consistency->>lock_consistency: break
                    end
                end
            end
        end
        lock_consistency-->>Caller: out
    end
Loading

File-Level Changes

Change Details Files
Introduce lock-consistency annotation logic that filters noisy numeric slots and computes belief-to-lock conflict mappings.
  • Add VERSION_KEYS and calendar year band constants to identify version-like keys and date-like values that should not produce conflicts.
  • Implement annotation_slots to drop multi-valued numeric keys, version-related keys, and calendar-year integer values while preserving enum slots.
  • Implement lock_conflict_annotations to map candidate belief ids to the first lock belief id they slot-conflict with, using suppressed slots on both sides and deterministic lock ordering.
  • Ensure the annotation path is pure and deterministic, with no store reads or external thresholds, and keep suppression local to this module rather than changing the shared extractor.
src/aelfrice/lock_consistency.py
Add targeted tests that pin suppression behavior and the lock-conflict annotation semantics, including symmetry and slot-scoped suppression.
  • Add helper fixtures to construct Belief instances and extract numeric/enum slots from text for tests.
  • Test each suppression rule individually: multi-valued keys dropped, single-valued keys kept, repeated identical values not treated as multi-valued, version keys and calendar-year integers suppressed, values outside the calendar band and non-integral year-range values preserved, and enum slots passed through.
  • Test the annotation behavior: genuine conflicts annotated, agreements not annotated, version/date locks no longer annotating everything, slot-scoped suppression where genuine conflicts alongside version literals still annotate, behavior when no locks exist or when beliefs have no conflicting slots, deterministic winner based on lock order, repeatability, and symmetric suppression on candidate and lock sides.
  • Cover mutation-found edge case where candidate-side suppression must run, ensuring passing raw candidate slots would fail only the dedicated symmetry test.
tests/test_lock_consistency_1175.py
Document the new lock-consistency annotation feature and its suppression design in the v4 changelog.
  • Add a detailed changelog entry describing the lock-consistency annotation feature, its measurement-backed suppression rules, and the fact that it annotates rather than drops beliefs.
  • Clarify that the suppression logic is implemented in src/aelfrice/lock_consistency.py and that extractor behavior in value_compare.extract_values remains unchanged.
  • Note that this PR implements only the library half (annotation logic) and that retrieval wiring and hook rendering will be done in a separate change.
CHANGELOG/v4.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Setr:2026-07-31T17:22:32Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Garsecg:2026-07-31T17:22:59Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Toug:2026-07-31T17:23:01Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Garsecg:2026-07-31T17:23:04Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Toug:2026-07-31T17:23:06Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Reviewed. The design is right and the reasoning is unusually well evidenced — refuting two candidate suppression rules with data before shipping the third is exactly the right order, and putting the filter here rather than narrowing value_compare.extract_values keeps the blast radius #1228 declined to take.

Mutation-tested all six load-bearing behaviours. Five of six fail a named test; one does not.

mutation result
drop multi-valued-key suppression test_multi_valued_key_is_dropped + 1
drop VERSION_KEYS suppression test_version_keys_are_dropped[v/version/rev/release] + 1
drop calendar-year suppression test_calendar_year_is_dropped
annotate on unfiltered candidate slots test_suppression_is_symmetric_not_lock_side_only
report last conflicting lock, not first test_first_lock_in_order_wins
annotate on unfiltered lock slots 0 failures

1. Lock-side filtering is load-bearing but untested

Replacing filtered_locks = [(lock, annotation_slots(slots)) …] with list(locked_pairs) passes all 24 tests. The docstring's promise — "Both sides are passed through annotation_slots here … so a caller cannot accidentally annotate on unsuppressed slots" — is currently documentation, not a pinned invariant.

It is not dead symmetry. The two suppressions keyed on vocabulary are symmetric enough that candidate-side filtering alone masks them, but the multi-valued rule is per-belief, so a key can be multi-valued in the lock and single-valued in the candidate:

lock  = ValueSlots(numeric=(NumericSlot("timeout", 30.0), NumericSlot("timeout", 60.0)), enum=())
cand  = ValueSlots(numeric=(NumericSlot("timeout", 45.0),), enum=())

find_conflicts(annotation_slots(cand), annotation_slots(lock))  # False  <- shipped
find_conflicts(annotation_slots(cand), lock)                    # True   <- spurious

That is precisely the annotation the multi-valued rule exists to prevent: a lock naming two different timeouts cannot disagree with anything. Worth a test — note test_suppression_is_symmetric_not_lock_side_only reads as if it covers this and covers the candidate side instead.

2. _is_calendar_year raises on non-finite input

annotation_slots(ValueSlots(numeric=(NumericSlot("x", float("inf")),), enum=()))
  -> OverflowError: cannot convert float infinity to integer
  -> ValueError:    cannot convert float NaN to integer   (for nan)

value != int(value) is unguarded. Not reachable through the shipped path_extract_numerics refuses non-finite at value_compare.py:225 (#1227/#1228), so slots from extract_values are always finite.

Raising it because this module's sibling already took the opposite decision on the identical hazard: _format_number (value_compare.py:351-357) carries the same int() narrowing behind if not math.isfinite(x), with the comment "this is the second line of defence, for any caller that reaches the comparator by another route." annotation_slots is exported in __all__ and takes a caller-constructed ValueSlots, so it is exactly such a route. One line, and it makes the convention uniform.

Neither blocks

Both are latent — today's only producer of ValueSlots guards the input, and the lock-side behaviour is correct as written. I would take them in the wiring PR rather than respin this one.

On the deferred consumer

Leaving retrieve_with_tiers / hook.py out is the right call and it is declared, so this is not the producer-without-consumer pattern. Flagging only because I audited #1162 earlier today and found four modules in that state — heat-kernel, edge_rerank (since wired), uri_baki, RetrievalCache. Worth an issue reference so the wiring PR is tracked rather than remembered.

Full suite green locally (6701 passed), CI green, CHANGELOG insert-only.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 31, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Setr:2026-07-31T17:27:36Z]

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 31, 2026
@github-actions
github-actions Bot merged commit 00b5eae into main Jul 31, 2026
35 of 42 checks passed
@github-actions

Copy link
Copy Markdown

merge-train: merged 00b5eaemain 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.

1 participant