Skip to content

feat(dedup): audit-only aelf doctor dedup (#197 R1) - #376

Merged
robotrocketscience merged 3 commits into
mainfrom
feat/issue-197-dedup-audit
May 3, 2026
Merged

feat(dedup): audit-only aelf doctor dedup (#197 R1)#376
robotrocketscience merged 3 commits into
mainfrom
feat/issue-197-dedup-audit

Conversation

@yoshi280

@yoshi280 yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator

First atomic PR for #197 (dedup module — v2.0 evaluation). Implements
R1: audit-only aelf doctor --dedup per the ratification at
#197
(2026-04-29: ship at v2.0, port research-line dedup.py stdlib-only,
defaults Jaccard 0.8 + Levenshtein 0.85, 5000-pair cap, audit-only
first then bench-gated write-path hook).

What

src/aelfrice/dedup.py — stdlib-only near-duplicate detector pairing
Jaccard over the existing lowercase Unicode-word tokenizer with a
two-row Levenshtein-distance ratio. Both thresholds must clear for a
pair to count as a near-duplicate; defaults match the ratification
(Jaccard ≥ 0.8, Levenshtein ratio ≥ 0.85, max 5000 pairs).

dedup_audit(store, ...) is read-only:

  • walks every belief pair via direct O(n²) Jaccard prefilter;
  • runs Levenshtein-ratio confirmation only on prefilter survivors;
  • collapses pairs into clusters via union-find;
  • returns a DedupAuditReport with deterministic ordering (sorted
    by (id_a, id_b), truncated at max_candidate_pairs).

aelf doctor --dedup prints the clustered report. Per-run flags
--dedup-jaccard, --dedup-levenshtein, --dedup-max-pairs
override config; the [dedup] section in .aelfrice.toml provides
project-level defaults via the same loader pattern as [rebuilder]
and [implicit_feedback].

What this PR does not change

  • No edges inserted, no beliefs mutated. The write-path
    SUPERSEDES hook is the bench-gated R2 deferred behind the v2.0
    corpus benchmark. R2 lands once [v2.0] Bench-gate harness — consume lab corpus, skip-when-absent on public CI #319's per-module bench gate
    shows neutral-or-positive p@k and recall on the existing lab
    corpus.
  • FTS5 candidate path was tried and abandoned. "don't" / "do
    not" tokenise into disjoint sets after the FTS5 tokenizer's
    apostrophe handling, so FTS5 misses the exact paraphrase shape
    this detector targets. Direct O(n²) Jaccard prefilter clears
    in seconds at the live-store median (~1.6k beliefs ≈ 1.3M pairs;
    set-intersection on small token sets ~1-10 µs/pair).

Verification

  • uv run pytest tests/test_dedup.py -q → 40 passed.
  • uv run pytest tests/ --ignore=tests/regression --ignore=tests/bench_gate --ignore=tests/e2e -q2139 passed, 14 skipped (no regression vs main).
  • Discretion grep over git diff origin/main...HEAD: clean.
  • uv tool run ruff check src/aelfrice/dedup.py tests/test_dedup.py → clean.
  • All commits SSH-signed.

Test plan

  • Unit tests pass on Python 3.13.
  • Wider test suite green; no regression.
  • CI staging-gate green.
  • CodeQL / vulture / deptry / typos green.

Refs

#197

Summary by Sourcery

Add an audit-only near-duplicate detection facility for beliefs and expose it via the doctor CLI, with configurable thresholds and reporting.

New Features:

  • Introduce a stdlib-only deduplication module that detects near-duplicate belief pairs using Jaccard and Levenshtein similarity and returns clustered audit reports.
  • Add the aelf doctor --dedup subcommand with configurable thresholds and pair caps to run the dedup audit and print a human-readable report.

Enhancements:

  • Load [dedup] configuration from .aelfrice.toml with validated defaults and graceful fallback behaviour.
  • Document the dedup audit command, configuration knobs, and output shape in dedicated documentation and the limitations overview.

Documentation:

  • Add docs/dedup.md describing the dedup audit command, configuration, thresholds, and limitations.
  • Update docs/LIMITATIONS.md to note that near-duplicate beliefs from different ingest paths persist and reference the new audit command.

Tests:

  • Add comprehensive unit and integration tests for the dedup module, configuration loader, report formatting, and CLI integration of aelf doctor --dedup.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

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

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ 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: e6912383-acb8-4679-8fe4-d50c674535bc

📥 Commits

Reviewing files that changed from the base of the PR and between e5ea00b and 931c20e.

📒 Files selected for processing (5)
  • docs/LIMITATIONS.md
  • docs/dedup.md
  • src/aelfrice/cli.py
  • src/aelfrice/dedup.py
  • tests/test_dedup.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-197-dedup-audit

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
Review rate limit: 0/1 reviews remaining, refill in 52 minutes and 51 seconds.

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

@sourcery-ai

sourcery-ai Bot commented May 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements an audit-only near-duplicate detection module (aelfrice.dedup) and wires it into aelf doctor --dedup, including configuration loading from .aelfrice.toml, deterministic clustering and reporting of duplicate belief pairs, CLI flags, documentation, and comprehensive unit/CLI/config tests, without mutating the belief store or graph.

Sequence diagram for aelf doctor dedup audit flow

sequenceDiagram
    actor User
    participant CLI as aelf_doctor
    participant Doctor as _cmd_doctor
    participant DedupCmd as _cmd_doctor_dedup
    participant Config as load_dedup_config
    participant Store as MemoryStore
    participant Dedup as dedup_audit
    participant Formatter as format_audit_report
    participant Out as out

    User->>CLI: run aelf doctor --dedup [--dedup-*]
    CLI->>Doctor: _cmd_doctor(args,out)
    Doctor->>DedupCmd: _cmd_doctor_dedup(args,out)

    DedupCmd->>Config: load_dedup_config()
    Config-->>DedupCmd: DedupConfig(defaults_or_toml_values)

    DedupCmd->>DedupCmd: apply CLI overrides to DedupConfig

    DedupCmd->>Store: _open_store()
    Store-->>DedupCmd: MemoryStore

    DedupCmd->>Dedup: dedup_audit(store,jaccard_min,levenshtein_min,max_candidate_pairs)
    Dedup-->>DedupCmd: DedupAuditReport

    DedupCmd->>Store: close()

    DedupCmd->>Formatter: format_audit_report(report)
    Formatter-->>DedupCmd: text_report

    DedupCmd->>Out: print(text_report)
    DedupCmd-->>Doctor: exit_code 0 or 1
    Doctor-->>CLI: exit_code
    CLI-->>User: dedup audit output
Loading

Class diagram for dedup audit types and helpers

classDiagram
    class DuplicatePair {
        +str belief_a_id
        +str belief_b_id
        +float jaccard_score
        +float levenshtein_score
    }

    class DuplicateCluster {
        +str representative_id
        +tuple~str~ member_ids
    }

    class DedupAuditReport {
        +int n_beliefs_scanned
        +int n_candidate_pairs
        +int n_duplicate_pairs
        +int n_clusters
        +bool truncated
        +tuple~DuplicatePair~ pairs
        +tuple~DuplicateCluster~ clusters
    }

    class DedupConfig {
        +float jaccard_min
        +float levenshtein_min
        +int max_candidate_pairs
    }

    class _UnionFind {
        -dict~str,str~ _parent
        -dict~str,int~ _size
        +__init__()
        +make(x str)
        +find(x str) str
        +union(a str,b str)
        +groups() dict~str,list~str~~
    }

    class MemoryStore {
        +list_beliefs_for_indexing() list~tuple~str,str~~
    }

    class dedup_module {
        +float DEFAULT_JACCARD_MIN
        +float DEFAULT_LEVENSHTEIN_MIN
        +int DEFAULT_MAX_CANDIDATE_PAIRS
        +jaccard(a set~str~,b set~str~) float
        +levenshtein_distance(a str,b str) int
        +levenshtein_ratio(a str,b str) float
        +dedup_audit(store MemoryStore,jaccard_min float,levenshtein_min float,max_candidate_pairs int) DedupAuditReport
        +cluster_pairs(pairs Iterable~DuplicatePair~) tuple~DuplicateCluster~
        +load_dedup_config(start Path) DedupConfig
        +format_audit_report(report DedupAuditReport) str
    }

    dedup_module --> DuplicatePair
    dedup_module --> DuplicateCluster
    dedup_module --> DedupAuditReport
    dedup_module --> DedupConfig
    dedup_module --> _UnionFind
    dedup_module --> MemoryStore

    DedupAuditReport "*" o-- "*" DuplicatePair
    DedupAuditReport "*" o-- "*" DuplicateCluster
Loading

File-Level Changes

Change Details Files
Add stdlib-only near-duplicate detection module with Jaccard + Levenshtein, clustering, config loading, and report formatting.
  • Introduce similarity primitives: Jaccard over token sets, two-row Levenshtein distance, and length-normalised Levenshtein ratio with well-defined edge cases.
  • Define data structures for duplicate pairs, duplicate clusters, and a DedupAuditReport summarizing one audit run, including counts, truncation flag, and deterministic ordering.
  • Implement O(n²) candidate generation using cached tokenisation and Jaccard prefiltering with deterministic truncation by (id_a, id_b) and a max_candidate_pairs cap.
  • Add a small union-find implementation and cluster_pairs helper to collapse duplicate pairs into connected components with deterministic representative IDs and member ordering.
  • Implement dedup_audit entry point over MemoryStore with argument validation, Jaccard prefilter + Levenshtein confirmation, and construction of a DedupAuditReport without mutating the store.
  • Introduce DedupConfig plus a .aelfrice.toml walk-up loader with robust error handling and typed parsing of [dedup] jaccard_min, levenshtein_min, and max_candidate_pairs, falling back to defaults on errors.
  • Add format_audit_report to render DedupAuditReport into a plain-text report that mirrors existing doctor report shapes, including handling of truncation and limiting printed pairs.
src/aelfrice/dedup.py
Wire the new dedup audit into the CLI as aelf doctor --dedup with configurable thresholds and pair cap.
  • Extend _cmd_doctor dispatcher to route when args.dedup is set before other doctor scopes.
  • Add _cmd_doctor_dedup helper that loads DedupConfig, applies CLI overrides for thresholds and max pairs, opens the store, runs dedup_audit with validated parameters, handles ValueError as an exit-1 error message to stderr, and prints the formatted report to the provided output stream.
  • Extend the doctor subparser with --dedup, --dedup-jaccard, --dedup-levenshtein, and --dedup-max-pairs flags, including help text that documents semantics, default sources, and interaction with the [dedup] config section.
src/aelfrice/cli.py
Document the dedup audit behavior and limitations in the user docs.
  • Add a dedicated docs/dedup.md page describing the dedup algorithm, usage, CLI flags, .aelfrice.toml configuration, defaults, output shape, and the separation between audit-only R1 and future write-path R2.
  • Update docs/LIMITATIONS.md to call out that near-duplicate paraphrases from different ingest paths persist, and that v2.0 ships audit-only aelf doctor --dedup while the write-path SUPERSEDES hook remains bench-gated.
docs/dedup.md
docs/LIMITATIONS.md
Add comprehensive tests for dedup primitives, audit behavior, config loader, report formatting, and CLI integration.
  • Add unit tests for jaccard, levenshtein_distance, levenshtein_ratio, and cluster_pairs covering edge cases, basic correctness, and deterministic representative selection.
  • Add tests for dedup_audit over a real MemoryStore fixture ensuring correct detection/non-detection of duplicates, behavior under different thresholds, clustering semantics, validation errors, and the audit-only (no mutation) contract.
  • Add tests for format_audit_report covering empty and non-empty reports and ensuring cluster and pair details appear in output.
  • Add tests for load_dedup_config handling of missing config, well-formed overrides, out-of-range values, wrong types, and malformed TOML, verifying fallback to defaults.
  • Add CLI integration tests that run aelfrice.cli.main with doctor --dedup against on-disk MemoryStore DBs, checking exit codes, clean-store output, duplicate detection, and threshold overrides via flags.
tests/test_dedup.py

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

@yoshi280 yoshi280 added the attn:review Needs review (PR open, awaiting reviewer) label May 3, 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.

Hey - I've found 3 issues, and left some high level feedback:

  • In format_audit_report, the truncation line always reports DEFAULT_MAX_CANDIDATE_PAIRS even when a different max_candidate_pairs was used for the run; consider using the value from report so the output reflects the actual cap in effect.
  • The dedup_audit docstring mentions degrading gracefully on FTS5 errors, but the implementation no longer touches FTS5; updating that wording would avoid confusion about what the function actually depends on and how failures are handled.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `format_audit_report`, the truncation line always reports `DEFAULT_MAX_CANDIDATE_PAIRS` even when a different `max_candidate_pairs` was used for the run; consider using the value from `report` so the output reflects the actual cap in effect.
- The `dedup_audit` docstring mentions degrading gracefully on FTS5 errors, but the implementation no longer touches FTS5; updating that wording would avoid confusion about what the function actually depends on and how failures are handled.

## Individual Comments

### Comment 1
<location path="src/aelfrice/dedup.py" line_range="319-328" />
<code_context>
+# --- Top-level audit entry point ---------------------------------------
+
+
+def dedup_audit(
+    store: MemoryStore,
+    *,
+    jaccard_min: float = DEFAULT_JACCARD_MIN,
+    levenshtein_min: float = DEFAULT_LEVENSHTEIN_MIN,
+    max_candidate_pairs: int = DEFAULT_MAX_CANDIDATE_PAIRS,
+) -> DedupAuditReport:
+    """Walk the store, find near-duplicate belief pairs, return a report.
+
+    Read-only: no edges are inserted, no beliefs are mutated. The
+    write-path hook (insert SUPERSEDES edges) is deferred behind the
+    #197 bench gate.
+
+    Raises `ValueError` on malformed thresholds; degrades gracefully
+    on per-belief FTS5 errors (skips that belief, logs nothing).
+    """
</code_context>
<issue_to_address>
**issue:** dedup_audit does not actually degrade gracefully on per-belief/tokenization errors

The current implementation will propagate exceptions from `store.list_beliefs_for_indexing()`, `tokenize()`, or `_jaccard_prefiltered_pairs`, so a single bad belief/tokenization error aborts the whole audit. To match the docstring’s “degrades gracefully” guarantee, consider catching per-belief failures at iteration/tokenization time and skipping those beliefs, while still raising on configuration issues (e.g. the existing `ValueError` threshold checks).
</issue_to_address>

### Comment 2
<location path="src/aelfrice/dedup.py" line_range="515-517" />
<code_context>
+    lines.append("=" * 40)
+    lines.append(f"Beliefs scanned         : {report.n_beliefs_scanned}")
+    lines.append(f"Candidate pairs visited : {report.n_candidate_pairs}")
+    if report.truncated:
+        lines.append(
+            f"  (truncated to {DEFAULT_MAX_CANDIDATE_PAIRS} — see "
+            f"[dedup] max_candidate_pairs)"
+        )
</code_context>
<issue_to_address>
**issue (bug_risk):** Truncation message always prints the default cap, ignoring configured/CLI max_candidate_pairs

When `report.truncated` is true, this always prints `DEFAULT_MAX_CANDIDATE_PAIRS`, even when the effective `max_candidate_pairs` was set via config or `--dedup-max-pairs`. That makes the report misleading (e.g., it may claim truncation at 5000 pairs when a different limit was used). Consider passing the effective `max_candidate_pairs` into `DedupAuditReport` and using that value here instead of the module-level default.
</issue_to_address>

### Comment 3
<location path="src/aelfrice/dedup.py" line_range="190-199" />
<code_context>
+def _jaccard_prefiltered_pairs(
</code_context>
<issue_to_address>
**suggestion:** Candidate pair count semantics differ from the docstring description

The docstring describes `raw_count` as "the raw candidate count (all O(n^2) pairs visited)", but the code skips pairs with blank content and only increments `raw_count` for the remaining ones. This makes `n_candidate_pairs` effectively a "non-empty pair" count rather than a full O(n^2) visit count. Either move the `raw_count` increment before the blank-content checks or update the docstring to match the current behavior.

Suggested implementation:

```python
        raw_count += 1

        if not content_a or not content_b:
            continue

```

If the blank-content check or `raw_count` increment is formatted differently in your actual code (e.g., different indentation, extra conditions, or comments between them), adjust the SEARCH/REPLACE block accordingly so that:

1. `raw_count += 1` is executed immediately after the pair `(content_a, content_b)` is formed / selected in the nested loops.
2. All subsequent `continue` statements that filter out blank content occur *after* incrementing `raw_count`, ensuring `raw_count` counts every visited pair, even those with blank content.
</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 src/aelfrice/dedup.py
Comment on lines +319 to +328
def dedup_audit(
store: MemoryStore,
*,
jaccard_min: float = DEFAULT_JACCARD_MIN,
levenshtein_min: float = DEFAULT_LEVENSHTEIN_MIN,
max_candidate_pairs: int = DEFAULT_MAX_CANDIDATE_PAIRS,
) -> DedupAuditReport:
"""Walk the store, find near-duplicate belief pairs, return a report.

Read-only: no edges are inserted, no beliefs are mutated. The

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue: dedup_audit does not actually degrade gracefully on per-belief/tokenization errors

The current implementation will propagate exceptions from store.list_beliefs_for_indexing(), tokenize(), or _jaccard_prefiltered_pairs, so a single bad belief/tokenization error aborts the whole audit. To match the docstring’s “degrades gracefully” guarantee, consider catching per-belief failures at iteration/tokenization time and skipping those beliefs, while still raising on configuration issues (e.g. the existing ValueError threshold checks).

Comment thread src/aelfrice/dedup.py
Comment on lines +515 to +517
if report.truncated:
lines.append(
f" (truncated to {DEFAULT_MAX_CANDIDATE_PAIRS} — see "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Truncation message always prints the default cap, ignoring configured/CLI max_candidate_pairs

When report.truncated is true, this always prints DEFAULT_MAX_CANDIDATE_PAIRS, even when the effective max_candidate_pairs was set via config or --dedup-max-pairs. That makes the report misleading (e.g., it may claim truncation at 5000 pairs when a different limit was used). Consider passing the effective max_candidate_pairs into DedupAuditReport and using that value here instead of the module-level default.

Comment thread src/aelfrice/dedup.py
Comment on lines +190 to +199
def _jaccard_prefiltered_pairs(
beliefs: list[tuple[str, str]],
*,
jaccard_min: float,
max_pairs: int,
) -> tuple[
list[tuple[str, str, str, str, frozenset[str], frozenset[str], float]],
int,
bool,
]:

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: Candidate pair count semantics differ from the docstring description

The docstring describes raw_count as "the raw candidate count (all O(n^2) pairs visited)", but the code skips pairs with blank content and only increments raw_count for the remaining ones. This makes n_candidate_pairs effectively a "non-empty pair" count rather than a full O(n^2) visit count. Either move the raw_count increment before the blank-content checks or update the docstring to match the current behavior.

Suggested implementation:

        raw_count += 1

        if not content_a or not content_b:
            continue

If the blank-content check or raw_count increment is formatted differently in your actual code (e.g., different indentation, extra conditions, or comments between them), adjust the SEARCH/REPLACE block accordingly so that:

  1. raw_count += 1 is executed immediately after the pair (content_a, content_b) is formed / selected in the nested loops.
  2. All subsequent continue statements that filter out blank content occur after incrementing raw_count, ensuring raw_count counts every visited pair, even those with blank content.

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:kulili:2026-05-03T15:57:15Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:Toug:2026-05-03T15:57:17Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:Toug:2026-05-03T15:57:22Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:Gylf:2026-05-03T15:57:29Z]

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

github-actions Bot commented May 3, 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-197-dedup-audit' && 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.

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:Gylf:2026-05-03T15:57:33Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:Setr:2026-05-03T15:57:36Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:Setr:2026-05-03T15:57:41Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

Reviewed: code is clean, deterministic, audit-only as advertised; all 18 checks green; signed; discretion grep on diff is clean. Sanitized one token in the PR body before noticing the FF state.

Branch is no longer FF — main moved to aa1aabb (#375 merged). Rebase needed before merge:

git fetch github main
git rebase github/main
git push --force-with-lease github feat/issue-197-dedup-audit

Releasing review claim. Will re-claim after rebase or another reviewer can pick it up.

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:kulili:2026-05-03T15:59:20Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:Kulili:2026-05-03T16:42:12Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:Kulili:2026-05-03T16:42:42Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:Setr:2026-05-03T16:48:26Z]

Stdlib-only port of the research-line dedup detector. Pairs Jaccard
over lowercase Unicode-word tokens (>= 0.8 default) with Levenshtein
ratio (>= 0.85 default) as the second-stage gate; both must clear
for a pair to count as a near-duplicate. Defaults from the #197
ratification (Jaccard 0.8, Levenshtein 0.85, max-pairs 5000).

`dedup_audit(store, ...)` is read-only: walks every belief pair via
direct O(n^2) Jaccard prefilter (the FTS5 path misses paraphrases
where stemming diverges, e.g. "don't" / "do not"), runs Levenshtein
ratio on prefilter survivors, returns a `DedupAuditReport` with
pairs + union-find-collapsed clusters. No edges inserted, no
beliefs mutated. Sampling is deterministic (sort by `(id_a, id_b)`,
truncate at `max_candidate_pairs`) so the same store produces the
same report across runs.

The CLI surface (`aelf doctor dedup`) and `[dedup]` config block
land in the next commit; the write-path SUPERSEDES hook is the
bench-gated R2 deferred behind the corpus benchmark.

32 tests: similarity primitives, union-find clustering, audit-pass
read-only contract, cluster chain collapse, threshold floor
behaviour (paraphrase rejection at default Jaccard, relaxed-mode
recovery), invalid-threshold guards, format_audit_report shape.
Wires the audit pass behind `aelf doctor --dedup`. New flags
`--dedup-jaccard`, `--dedup-levenshtein`, `--dedup-max-pairs`
override the per-run thresholds; `[dedup]` block in `.aelfrice.toml`
provides project-level defaults. Read-only — no edges inserted, no
beliefs mutated; the write-path SUPERSEDES hook is the bench-gated
R2 deferred behind the corpus benchmark per #197 ratification.

Config loader follows the `[rebuilder]` / `[implicit_feedback]`
convention: walk up from cwd looking for `.aelfrice.toml`,
malformed values degrade to defaults with a stderr trace, never
raises.

8 new tests: TOML loader (default fallthrough, well-formed
override, out-of-range fallback, wrong-type fallback,
malformed-TOML fallback) + CLI integration (clean store exit 0,
pair detection through main(), threshold overrides via flags).
Existing 100 cli/doctor tests still pass.
`docs/dedup.md`: usage, per-run flags, [dedup] config block,
defaults table, output-shape sample, explicit "audit-only by
design — write-path is bench-gated R2" call-out.

LIMITATIONS § Sharp edges: new bullet on near-duplicates from
different ingest paths, pointing at the new audit surface.
Tightens but does not remove the existing onboard-non-incremental
caveat — the audit lists clusters, it does not collapse them.
@robotrocketscience
robotrocketscience force-pushed the feat/issue-197-dedup-audit branch from 5809301 to 931c20e Compare May 3, 2026 16:49
@robotrocketscience
robotrocketscience merged commit 931c20e into main May 3, 2026
18 checks passed
@robotrocketscience
robotrocketscience deleted the feat/issue-197-dedup-audit branch May 3, 2026 16:51
@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:Setr:2026-05-03T16:51:21Z]

@yoshi280 yoshi280 removed the attn:merge-conflict PR branch needs rebase label May 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:review Needs review (PR open, awaiting reviewer)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants