Skip to content

feat(clustering): RetrievalCluster module + multi-fact corpus mount (436) - #496

Merged
robotrocketscience merged 2 commits into
mainfrom
feat/issue-436-clustering
May 8, 2026
Merged

feat(clustering): RetrievalCluster module + multi-fact corpus mount (436)#496
robotrocketscience merged 2 commits into
mainfrom
feat/issue-436-clustering

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Module-first slice of intentional clustering per
docs/feature-intentional-clustering.md. Ships the pure library
(src/aelfrice/clustering.py), the MemoryStore.edges_for_beliefs
batched fetch, the multi_fact corpus mount, and the bench-gate
harness scaffold. Retrieval-side wiring is the next gate.

Toward 436.

What landed

  • src/aelfrice/clustering.pyRetrievalCluster dataclass,
    cluster_candidates() (path-compressed union-find on candidate-
    induced edge subgraph), pack_with_clusters() (diversity-aware
    greedy fill: Stage 1 representatives, Stage 2 score-ranked tail).
    Defaults: DEFAULT_CLUSTER_EDGE_FLOOR = 0.4 (CITES in, RELATES_TO
    out per EDGE_VALENCE), DEFAULT_CLUSTER_DIVERSITY_TARGET = 3.
  • MemoryStore.edges_for_beliefs(belief_ids) — batched fetch:
    one SQL, returns every edge whose src OR dst is in the input.
  • Bench-gate scaffold at tests/bench_gate/test_intentional_clustering.py
    • corpus mount at tests/corpus/v2_0/multi_fact/. Public CI skips
      via the autouse bench_gated marker; labelled rows live lab-side.
  • Spec status updated from "spec, no implementation" to
    "module shipped; retrieval wiring + bench-gate evidence next gates".

What's deferred (next PR)

  • use_intentional_clustering kwarg / env / TOML flag-resolution.
  • retrieve_v2 integration (replace pack loop behind default-OFF flag).
  • Locked-belief pre-include in front of Stage 1 per spec § Open Q4.
  • Latency microbench (spec § A4) once integration lands.

The split exists because the pack-loop edit touches the hot retrieval
path. Shipping the substrate first lets reviewers verify the module
without a hot-path-edit blocker; the wiring PR is the next ship gate.

Test plan

  • 16 new clustering tests (cluster_candidates partitioning,
    edge filtering, score-ranked member ordering, pack Stage 1+2,
    strict-diversity mode, missing-id race, default constants).
  • MemoryStore.edges_for_beliefs batched-lookup test.
  • tests/test_corpus_schema.py accepts the new multi_fact
    module schema (with list[list_str] validator for
    expected_clusters).
  • tests/bench_gate/test_intentional_clustering.py skips cleanly
    when AELFRICE_CORPUS_ROOT is unset.
  • Full suite 2878 passed / 45 skipped — no regressions.
  • Bench-gate run against lab corpus — gated on the wiring PR
    landing first; the harness can't measure cluster_coverage@k
    without use_intentional_clustering=ON flowing through
    retrieve_v2.

Summary by Sourcery

Introduce an intentional clustering module for retrieval along with supporting store APIs, corpus schema, and bench-gate scaffolding to validate multi-fact recall improvements.

New Features:

  • Add RetrievalCluster dataclass and clustering/packing APIs to support diversity-aware retrieval packing.
  • Add MemoryStore.edges_for_beliefs for batched edge lookup over belief IDs.
  • Define a new multi_fact graded corpus module for intentional clustering evaluation, with associated schema and validators.
  • Add a bench-gated intentional clustering test harness that validates the multi_fact corpus shape and wires to a clustering uplift runner when present.

Enhancements:

  • Update the intentional clustering feature spec to reflect that the core module has shipped.

Tests:

  • Add comprehensive unit tests for clustering behavior, token-budgeted packing, and the new edges_for_beliefs store API.

Summary by CodeRabbit

  • New Features

    • Introduced intentional clustering capability for retrieval reranking, enabling configurable diversity-focused candidate selection with token budget constraints.
  • Documentation

    • Updated feature documentation and corpus schema to reflect new clustering module requirements and evaluation criteria.
  • Tests

    • Added comprehensive test coverage for clustering operations, packing strategy, and corpus validation.

@sourcery-ai

sourcery-ai Bot commented May 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces the intentional clustering substrate as a standalone module and supporting infrastructure: a new clustering library with union-find based candidate clustering and a diversity-aware pack loop, a batched edge fetch helper in MemoryStore, a multi_fact corpus module plus schema support and docs for the bench gate, and scaffolding tests/bench hooks so this can ship independently of retrieval wiring.

Sequence diagram for MemoryStore.edges_for_beliefs batched edge fetch

sequenceDiagram
    participant Caller
    participant MemoryStore
    participant SQLiteConnection

    Caller->>MemoryStore: edges_for_beliefs(belief_ids)
    alt empty belief_ids
        MemoryStore-->>Caller: empty_edge_list
    else nonempty belief_ids
        MemoryStore->>MemoryStore: build_placeholders_and_params(belief_ids)
        MemoryStore->>SQLiteConnection: execute(SELECT_edges_src_or_dst_in_placeholders, params)
        SQLiteConnection-->>MemoryStore: edge_rows
        MemoryStore->>MemoryStore: edges = map_rows_to_edges(edge_rows)
        MemoryStore-->>Caller: edges
    end
Loading

Class diagram for the new intentional clustering module

classDiagram
    class Belief
    class Edge

    class RetrievalCluster {
        +int cluster_id
        +tuple[str,...] member_ids
        +str representative_id
        +float seed_score
    }

    class _UnionFind {
        -dict[str,str] _parent
        -dict[str,int] _size
        +__init__() void
        +make(x str) void
        +find(x str) str
        +union(a str, b str) void
    }

    class clustering_module {
        <<module>>
        +float DEFAULT_CLUSTER_EDGE_FLOOR
        +int DEFAULT_CLUSTER_DIVERSITY_TARGET
        +float _CHARS_PER_TOKEN
        +_belief_tokens(b Belief) int
        +cluster_candidates(candidates list[Belief], candidate_scores dict[str,float], edges Iterable[Edge], edge_weight_floor float) list[RetrievalCluster]
        +pack_with_clusters(clusters list[RetrievalCluster], belief_by_id dict[str,Belief], token_budget int, cluster_diversity_target int, fallback_to_score bool) list[Belief]
    }

    clustering_module ..> Belief : uses
    clustering_module ..> Edge : uses
    clustering_module ..> RetrievalCluster : creates
    clustering_module ..> _UnionFind : uses
    RetrievalCluster o--> Belief : members
Loading

Flow diagram for pack_with_clusters diversity-aware greedy fill

flowchart TD
    A[Start pack_with_clusters] --> B[Initialize out, used_tokens, seen, covered_clusters]
    B --> C[Sort clusters by descending seed_score]
    C --> D{Stage 1: more clusters and covered_clusters < cluster_diversity_target?}
    D -->|No| G[Stage 2 setup]
    D -->|Yes| E[Select next cluster and representative_id]
    E --> F{Representative exists, unseen, and fits token_budget?}
    F -->|No and fallback_to_score| G
    F -->|No and not fallback_to_score| D
    F -->|Yes| H[Append representative, update seen, used_tokens, covered_clusters]
    H --> D
    G --> I[Iterate clusters in descending seed_score]
    I --> J[Iterate member_ids within each cluster]
    J --> K{Member unseen, exists, and fits remaining budget?}
    K -->|No| J
    K -->|Yes| L[Append belief, update seen and used_tokens]
    L --> J
    J --> M{More clusters to scan?}
    M -->|Yes| I
    M -->|No| N[Return out]
Loading

File-Level Changes

Change Details Files
Add RetrievalCluster data model and clustering/packing algorithms implementing intentional clustering as a pure library module.
  • Introduce DEFAULT_CLUSTER_EDGE_FLOOR and DEFAULT_CLUSTER_DIVERSITY_TARGET tuning constants for clustering behaviour.
  • Implement cluster_candidates using a path-compressed union-find over the candidate-induced edge subgraph, filtering edges by weight and candidate membership, and producing deterministically ordered clusters.
  • Implement pack_with_clusters to perform a two-stage diversity-aware greedy pack under a token budget, with configurable diversity target and fallback behaviour, using a local token-estimation helper.
  • Include an internal _UnionFind implementation and a _belief_tokens helper to keep clustering independent of existing retrieval/dedup modules.
src/aelfrice/clustering.py
Add batched edge retrieval API to MemoryStore to support clustering edge lookups.
  • Implement edges_for_beliefs that returns all edges whose src or dst is in a provided belief id list using a single SQL query with IN clauses.
  • Ensure empty input short-circuits to avoid unnecessary SQL queries.
  • Add tests verifying correct edges are returned for various belief id sets, including empty and unmatched inputs.
src/aelfrice/store.py
tests/test_clustering.py
Extend the v2_0 test corpus with a multi_fact module and schema support for clustering evaluation.
  • Document the new multi_fact corpus directory, its fields, and the ship gate criteria for intentional clustering, including cluster_coverage@k uplift requirements.
  • Register the multi_fact module shape in the corpus schema with fields for query, expected_belief_ids, expected_clusters, n_clusters_required, and tag.
  • Introduce a list[list_str] validator to enforce structure of expected_clusters as non-empty lists of non-empty strings.
tests/corpus/v2_0/README.md
tests/test_corpus_schema.py
Add tests for the clustering module behaviour and bench-gate scaffolding for the intentional clustering ship gate.
  • Create unit tests covering cluster_candidates edge filtering, partitioning, deterministic ordering, and default edge floor behaviour.
  • Create unit tests covering pack_with_clusters Stage 1 and Stage 2 interaction, strict-diversity vs fallback behaviour, token-budget handling, and missing-id races.
  • Add a bench_gated test that smoke-checks the multi_fact corpus parsing and the presence of the clustering uplift runner, skipping appropriately when corpus or runner are unavailable.
  • Assert that the uplift runner, when present, enforces strictly positive cluster_coverage@k uplift for use_intentional_clustering=ON vs OFF.
tests/test_clustering.py
tests/bench_gate/test_intentional_clustering.py
Update intentional clustering feature spec status to reflect that the module has shipped while wiring and evidence remain pending.
  • Change feature-intentional-clustering status string from "spec, no implementation" to note the shipped module and remaining gates for retrieval wiring and bench evidence.
docs/feature-intentional-clustering.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

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@robotrocketscience has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 31 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d4051d24-e922-47b4-87a4-2dd39b240f12

📥 Commits

Reviewing files that changed from the base of the PR and between 271d630 and 5465b3c.

📒 Files selected for processing (1)
  • tests/bench_gate/test_intentional_clustering.py
📝 Walkthrough

Walkthrough

This PR implements the "intentional clustering" feature for retrieval-time reranking. It adds clustering algorithms via union-find connected components, two-stage greedy packing under token budgets, storage integration for edge batching, comprehensive unit tests, and corpus schema validation for the new multi_fact evaluation module.

Changes

Intentional Clustering Implementation

Layer / File(s) Summary
Data Contracts
src/aelfrice/clustering.py
RetrievalCluster dataclass with cluster_id, member_ids, representative_id, and seed_score fields establishes the clustering output contract.
Core Clustering Algorithms
src/aelfrice/clustering.py
cluster_candidates performs union-find to detect connected components from candidate-induced edge subgraph (filtering by edge-weight floor), ranks members by score, and returns deterministically sorted clusters. pack_with_clusters executes two-stage greedy selection: Stage 1 selects cluster representatives up to diversity target; Stage 2 fills remaining token budget from remaining members.
Storage Integration
src/aelfrice/store.py
MemoryStore.edges_for_beliefs(belief_ids) returns edges touching any input belief via src IN (...) OR dst IN (...) query.
Unit Tests
tests/test_clustering.py
Test helpers, cluster_candidates tests (empty input, edge-floor filtering, candidate-induced subgraph, member ordering, representative tie-breaking), pack_with_clusters tests (diversity limits, token budget enforcement, stage fallback, member filtering), and edges_for_beliefs integration tests with sqlite-backed store.
Corpus Schema Extension
tests/test_corpus_schema.py, tests/corpus/v2_0/README.md
multi_fact module added to schema with required fields (query, expected_belief_ids, expected_clusters, n_clusters_required, tag). Field validation extended to support list[list_str] type. Directory layout and ship-gate specification documented.
Benchmark Gate Tests
tests/bench_gate/test_intentional_clustering.py
CI-gated smoke tests validate multi_fact corpus round-trip loading and optional clustering uplift runner execution with cluster_coverage_uplift > 0 assertion and ON/OFF diagnostic messaging.
Documentation
docs/feature-intentional-clustering.md
Feature status updated from "spec, no implementation" to "module shipped" with remaining gates noted.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • robotrocketscience/aelfrice#436: This PR directly implements the intentional clustering feature requested in issue #436, delivering the cluster_candidates, pack_with_clusters, and edges_for_beliefs functionality along with comprehensive tests and corpus schema support.

Possibly related PRs

  • robotrocketscience/aelfrice#455: Related spec/design PR that documents the intentional clustering approach; this PR realizes that specification with the core implementation.
  • robotrocketscience/aelfrice#311: Both PRs extend the v2.0 corpus schema and test harness; this PR adds the multi_fact module extension to that shared scaffold.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: shipping a new RetrievalCluster module with clustering functionality and corpus integration for issue #436.
Description check ✅ Passed The description comprehensively covers all template sections: summary, linked issues (#436), type of change (feat), verification steps completed, detailed test plan, and notes on deferred work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-436-clustering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@robotrocketscience robotrocketscience added author-Toug PR coordination mutex attn:review Needs review (PR open, awaiting reviewer) labels May 8, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Gylf:2026-05-08T19:32:22Z]

@github-actions github-actions Bot added the attn:merge-conflict PR branch needs rebase label May 8, 2026
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

This PR is now behind main. Rebase locally so your commit signatures stay intact:

git fetch origin && git checkout 'feat/issue-436-clustering' && git rebase origin/main
# resolve conflicts if any, then
git push --force-with-lease

Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the required_signatures rule on main then blocks the merge. See #341.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Setr:2026-05-08T19:33:04Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Setr:2026-05-08T19:33:09Z]

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Security issues:

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

General comments:

  • In edges_for_beliefs, consider guarding against very large belief_ids inputs (e.g. chunking the query or asserting on size) to avoid hitting SQLite’s parameter limit when constructing the IN clause.
  • Both _belief_tokens and _UnionFind duplicate logic that already exists elsewhere (retrieval._belief_tokens / dedup._UnionFind); it may be worth consolidating these into a shared utility to prevent future drift between implementations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `edges_for_beliefs`, consider guarding against very large `belief_ids` inputs (e.g. chunking the query or asserting on size) to avoid hitting SQLite’s parameter limit when constructing the `IN` clause.
- Both `_belief_tokens` and `_UnionFind` duplicate logic that already exists elsewhere (`retrieval._belief_tokens` / `dedup._UnionFind`); it may be worth consolidating these into a shared utility to prevent future drift between implementations.

## Individual Comments

### Comment 1
<location path="src/aelfrice/clustering.py" line_range="92-99" />
<code_context>
+            self._parent[x] = x
+            self._size[x] = 1
+
+    def find(self, x: str) -> str:
+        path: list[str] = []
+        while self._parent[x] != x:
+            path.append(x)
+            x = self._parent[x]
+        for p in path:
+            self._parent[p] = x
+        return x
+
+    def union(self, a: str, b: str) -> None:
</code_context>
<issue_to_address>
**suggestion:** Consider making _UnionFind.find more defensive when called with unknown elements.

`find` currently assumes `x` is in `_parent` and will raise `KeyError` otherwise. Since `_UnionFind` may be used elsewhere, it would be safer to either assert `x in _parent` for a clearer failure, or treat unknown elements as their own singleton (i.e., perform a lazy `make` inside `find`).

```suggestion
    def find(self, x: str) -> str:
        if x not in self._parent:
            self.make(x)

        path: list[str] = []
        while self._parent[x] != x:
            path.append(x)
            x = self._parent[x]
        for p in path:
            self._parent[p] = x
        return x
```
</issue_to_address>

### Comment 2
<location path="src/aelfrice/clustering.py" line_range="155-162" />
<code_context>
+    for cid in candidate_ids:
+        groups.setdefault(uf.find(cid), []).append(cid)
+
+    raw_clusters: list[tuple[float, str, tuple[str, ...]]] = []
+    for members in groups.values():
+        ranked = sorted(
+            members,
+            key=lambda mid: (-candidate_scores.get(mid, 0.0), mid),
+        )
+        seed = candidate_scores.get(ranked[0], 0.0)
+        raw_clusters.append((seed, ranked[0], tuple(ranked)))
+
+    raw_clusters.sort(key=lambda t: (-t[0], t[1]))
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Missing scores default to 0.0, which can silently hide data issues.

Using `candidate_scores.get(mid, 0.0)` in the sort key and for `seed` means missing scores are treated as 0 and won’t surface mismatches between `candidate_ids` and `candidate_scores`. If `candidate_scores` is meant to be complete, prefer `candidate_scores[mid]` or add a check/assert that all `candidate_ids` have entries so inconsistencies fail fast.

Suggested implementation:

```python
    groups: dict[str, list[str]] = {}
    for cid in candidate_ids:
        groups.setdefault(uf.find(cid), []).append(cid)

    # Ensure all candidate_ids have corresponding scores to avoid silently
    # treating missing scores as 0.0, which can hide data issues.
    missing_scores = [cid for cid in candidate_ids if cid not in candidate_scores]
    if missing_scores:
        raise ValueError(
            f"Missing scores for candidate_ids: {', '.join(sorted(missing_scores))}"
        )

    raw_clusters: list[tuple[float, str, tuple[str, ...]]] = []

```

```python
        ranked = sorted(
            members,
            key=lambda mid: (-candidate_scores[mid], mid),
        )
        seed = candidate_scores[ranked[0]]

```
</issue_to_address>

### Comment 3
<location path="src/aelfrice/store.py" line_range="2450-2453" />
<code_context>
        cur = self._conn.execute(
            f"SELECT * FROM edges WHERE src IN ({ph}) OR dst IN ({ph})",
            params,
        )
</code_context>
<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

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

Comment on lines +92 to +99
def find(self, x: str) -> str:
path: list[str] = []
while self._parent[x] != x:
path.append(x)
x = self._parent[x]
for p in path:
self._parent[p] = x
return x

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider making _UnionFind.find more defensive when called with unknown elements.

find currently assumes x is in _parent and will raise KeyError otherwise. Since _UnionFind may be used elsewhere, it would be safer to either assert x in _parent for a clearer failure, or treat unknown elements as their own singleton (i.e., perform a lazy make inside find).

Suggested change
def find(self, x: str) -> str:
path: list[str] = []
while self._parent[x] != x:
path.append(x)
x = self._parent[x]
for p in path:
self._parent[p] = x
return x
def find(self, x: str) -> str:
if x not in self._parent:
self.make(x)
path: list[str] = []
while self._parent[x] != x:
path.append(x)
x = self._parent[x]
for p in path:
self._parent[p] = x
return x

Comment on lines +155 to +162
raw_clusters: list[tuple[float, str, tuple[str, ...]]] = []
for members in groups.values():
ranked = sorted(
members,
key=lambda mid: (-candidate_scores.get(mid, 0.0), mid),
)
seed = candidate_scores.get(ranked[0], 0.0)
raw_clusters.append((seed, ranked[0], tuple(ranked)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Missing scores default to 0.0, which can silently hide data issues.

Using candidate_scores.get(mid, 0.0) in the sort key and for seed means missing scores are treated as 0 and won’t surface mismatches between candidate_ids and candidate_scores. If candidate_scores is meant to be complete, prefer candidate_scores[mid] or add a check/assert that all candidate_ids have entries so inconsistencies fail fast.

Suggested implementation:

    groups: dict[str, list[str]] = {}
    for cid in candidate_ids:
        groups.setdefault(uf.find(cid), []).append(cid)

    # Ensure all candidate_ids have corresponding scores to avoid silently
    # treating missing scores as 0.0, which can hide data issues.
    missing_scores = [cid for cid in candidate_ids if cid not in candidate_scores]
    if missing_scores:
        raise ValueError(
            f"Missing scores for candidate_ids: {', '.join(sorted(missing_scores))}"
        )

    raw_clusters: list[tuple[float, str, tuple[str, ...]]] = []
        ranked = sorted(
            members,
            key=lambda mid: (-candidate_scores[mid], mid),
        )
        seed = candidate_scores[ranked[0]]

Comment thread src/aelfrice/store.py
Comment on lines +2450 to +2453
cur = self._conn.execute(
f"SELECT * FROM edges WHERE src IN ({ph}) OR dst IN ({ph})",
params,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Source: opengrep

Comment thread tests/bench_gate/test_intentional_clustering.py Fixed
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:kulili:2026-05-08T19:34:11Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:kulili:2026-05-08T19:34:16Z]

…436)

Implements the pure-library half of intentional clustering per
docs/feature-intentional-clustering.md. New `src/aelfrice/clustering.py`
exposes:

- `RetrievalCluster` dataclass — dense cluster_id, score-ranked
  member_ids, representative_id, seed_score.
- `cluster_candidates()` — path-compressed union-find pass over the
  candidate-induced edge subgraph. Edges below `edge_weight_floor`
  (default 0.4: includes CITES, excludes RELATES_TO) and edges with
  one endpoint outside the candidate pool are filtered out.
- `pack_with_clusters()` — diversity-aware greedy fill. Stage 1 picks
  one representative per cluster (descending seed_score) until
  `cluster_diversity_target=3` distinct clusters are covered or budget
  is exhausted. Stage 2 fills remaining budget from the score-ranked
  tail. `fallback_to_score=True` (default) bails Stage 1 on the first
  budget miss.

`MemoryStore.edges_for_beliefs(belief_ids)` — batched fetch of every
edge whose src OR dst is in `belief_ids`. Single SQL with `IN (...)`,
empty input → empty list (no SQL).

Bench-gate harness scaffold at `tests/bench_gate/test_intentional_clustering.py`
+ corpus mount point at `tests/corpus/v2_0/multi_fact/`. Public CI
skips via the autouse `bench_gated` marker; corpus content lives
lab-side per the directory-of-origin rule.

The retrieval-side wiring (use_intentional_clustering flag in
retrieve_v2 + flag-resolution helpers) is the next gate; the module
ships independently so the substrate lands without a hot-path edit.
Spec status updated to "module shipped; retrieval wiring + bench-gate
evidence are the next gates".

Reuses the path-compressed union-find pattern from src/aelfrice/dedup.py
rather than importing it — neither owns the primitive yet, and a
future refactor can promote one of the two.
@robotrocketscience
robotrocketscience force-pushed the feat/issue-436-clustering branch from 491b04c to 271d630 Compare May 8, 2026 19:36
CodeQL flagged 'run_clustering_uplift may be uninitialized' on the
try/import + pytest.skip pattern — CodeQL flow analysis can't see that
pytest.skip raises. pytest.importorskip is the idiomatic equivalent
that returns the module on success and skips otherwise, with no
post-import name-binding ambiguity.
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Reviewed at the rebased + fix-up tip. Two reviewer-side commits:

  • 271d630 — clean rebase of Toug's feat(clustering): RetrievalCluster module + multi-fact corpus mount (436) onto current main (no conflicts; main moved by 5 vocab_bridge commits since branch base).
  • 5465b3cfix(test): use pytest.importorskip in clustering bench-gate (#436). CodeQL flagged "Potentially uninitialized local variable run_clustering_uplift" on the try/import + pytest.skip pattern — CodeQL flow analysis can't see that pytest.skip raises. pytest.importorskip is the idiomatic equivalent that returns the module on success and skips otherwise; no name-binding ambiguity. Test still skips cleanly when the lab-side runner is absent.

Both commits signed (G). Full suite locally 2886 passed / 25 skipped. Discretion clean.

Substance vs spec — clean. Module half of the contract matches docs/feature-intentional-clustering.md. Union-find pass is candidate-induced (spec § Open Q1), singletons get Stage-1 slots (Q2), tie-breaks deterministic by id ASC. MemoryStore.edges_for_beliefs returns the boundary plus the candidate-induced subgraph (more than the clusterer needs; the extra is filtered cheaply at clustering time).

Two stage-2 nuances to address in the wiring PR — non-blocking here:

  1. Stage-2 traversal is cluster-major, not score-flat. clustering.py:228-241 walks sorted_clusters (descending seed_score), within each cluster walks member_ids (descending member-score). That puts a low-scoring belief from a high-seed cluster before a high-scoring belief from a low-seed cluster — diverges from the spec snippet's "fill remaining budget by score" reading. Likely fine empirically (within-cluster scores correlate with seed) but it's a real behavioral difference and bench evidence will reflect it. Decide explicitly in the wiring PR: keep cluster-major (with a comment justifying), or sort score_ranked_remaining flat by candidate_scores.

  2. Stage-2 continue instead of break on budget overflow. clustering.py:238 keeps trying smaller beliefs after one doesn't fit; the spec snippet (§ Pack algorithm) breaks. Matters when the candidate pool has a wide token-size variance — current behavior is more recall-friendly (greedy fits anything that still fits) but is a deviation worth calling out in the spec or the code.

SQL-parameter-count side-note. edges_for_beliefs builds 2 * len(belief_ids) placeholders. SQLite default SQLITE_MAX_VARIABLE_NUMBER is 999 on older builds (32766 newer). At the spec's typical N≤200 candidate pool this is comfortable; a defensive batch-and-merge at 250 would be a v2.x hardening, not a Phase-1 blocker.

Locked-belief pre-include (spec § Open Q4) is correctly deferred per the PR body's "What's deferred" — the wiring PR adds the L0-skip at the top of the algorithm.

Merging once CI on the fix commit clears.

@robotrocketscience
robotrocketscience merged commit 5465b3c into main May 8, 2026
18 of 19 checks passed
@robotrocketscience
robotrocketscience deleted the feat/issue-436-clustering branch May 8, 2026 19:44
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Gylf:2026-05-08T19:44:30Z]

robotrocketscience added a commit that referenced this pull request May 8, 2026
CodeQL flags 'run_doc_linker_uplift may be uninitialized' on the
try/import + pytest.skip pattern — flow analysis can't see that
pytest.skip raises. pytest.importorskip is the idiomatic equivalent
that returns the module on success and skips otherwise, with no
post-import name-binding ambiguity.

Same fix as 5465b3c applied to the clustering bench-gate at #496.
robotrocketscience added a commit that referenced this pull request May 10, 2026
Substrate landed in v2.0/v2.1 (#496 module + corpus, #498 retrieve_v2
wiring, #504 bench-gate scorer). Bench gate cleared on the production
multi-store sweep (R6 60/60 PASS at p99 0.328ms, 15-30x margin under
the 5ms A4 budget). Per the resolve_use_intentional_clustering()
docstring contract ("the bench gate flips the default after lab-side
benchmark evidence clears"), the default is unblocked.

Precedence (env > kwarg > TOML > default) is unchanged; only the
default value flips False -> True. Reversible via
[retrieval] use_intentional_clustering = false in .aelfrice.toml or
AELFRICE_INTENTIONAL_CLUSTERING=0 for v2.0.x parity.

Tests updated:
- test_default_is_off -> test_default_is_on
- test_env_garbage_falls_through default-arm assertion flipped
- test_default_call_byte_identical_to_explicit_off ->
  test_default_call_byte_identical_to_explicit_on (ON-byte-identity
  invariant supersedes the earlier OFF-byte-identity invariant)

Floor parameter (DEFAULT_CLUSTER_EDGE_FLOOR = 0.4) is unchanged in
this commit. Lab finding (raising to 0.6 triples uplift on 8 rows
by excluding CITES inter-cluster edges) is queued as a separate
follow-up so this PR stays minimal and reviewable.
robotrocketscience added a commit that referenced this pull request May 10, 2026
Substrate landed in v2.0/v2.1 (#496 module + corpus, #498 retrieve_v2
wiring, #504 bench-gate scorer). Bench gate cleared on the production
multi-store sweep (R6 60/60 PASS at p99 0.328ms, 15-30x margin under
the 5ms A4 budget). Per the resolve_use_intentional_clustering()
docstring contract ("the bench gate flips the default after lab-side
benchmark evidence clears"), the default is unblocked.

Precedence (env > kwarg > TOML > default) is unchanged; only the
default value flips False -> True. Reversible via
[retrieval] use_intentional_clustering = false in .aelfrice.toml or
AELFRICE_INTENTIONAL_CLUSTERING=0 for v2.0.x parity.

Tests updated:
- test_default_is_off -> test_default_is_on
- test_env_garbage_falls_through default-arm assertion flipped
- test_default_call_byte_identical_to_explicit_off ->
  test_default_call_byte_identical_to_explicit_on (ON-byte-identity
  invariant supersedes the earlier OFF-byte-identity invariant)

Floor parameter (DEFAULT_CLUSTER_EDGE_FLOOR = 0.4) is unchanged in
this commit. Lab finding (raising to 0.6 triples uplift on 8 rows
by excluding CITES inter-cluster edges) is queued as a separate
follow-up so this PR stays minimal and reviewable.
robotrocketscience added a commit that referenced this pull request May 10, 2026
Substrate landed in v2.0/v2.1 (#496 module + corpus, #498 retrieve_v2
wiring, #504 bench-gate scorer). Bench gate cleared on the production
multi-store sweep (R6 60/60 PASS at p99 0.328ms, 15-30x margin under
the 5ms A4 budget). Per the resolve_use_intentional_clustering()
docstring contract ("the bench gate flips the default after lab-side
benchmark evidence clears"), the default is unblocked.

Precedence (env > kwarg > TOML > default) is unchanged; only the
default value flips False -> True. Reversible via
[retrieval] use_intentional_clustering = false in .aelfrice.toml or
AELFRICE_INTENTIONAL_CLUSTERING=0 for v2.0.x parity.

Tests updated:
- test_default_is_off -> test_default_is_on
- test_env_garbage_falls_through default-arm assertion flipped
- test_default_call_byte_identical_to_explicit_off ->
  test_default_call_byte_identical_to_explicit_on (ON-byte-identity
  invariant supersedes the earlier OFF-byte-identity invariant)

Floor parameter (DEFAULT_CLUSTER_EDGE_FLOOR = 0.4) is unchanged in
this commit. Lab finding (raising to 0.6 triples uplift on 8 rows
by excluding CITES inter-cluster edges) is queued as a separate
follow-up so this PR stays minimal and reviewable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:merge-conflict PR branch needs rebase attn:review Needs review (PR open, awaiting reviewer) author-Toug PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants