Skip to content

feat: add TLSH similarity clustering (offsetscan cluster) — 0.3.0 - #21

Merged
warpedatom merged 1 commit into
mainfrom
feat/tlsh-cluster-0.3.0
Jul 27, 2026
Merged

feat: add TLSH similarity clustering (offsetscan cluster) — 0.3.0#21
warpedatom merged 1 commit into
mainfrom
feat/tlsh-cluster-0.3.0

Conversation

@warpedatom

@warpedatom warpedatom commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Add a new TLSH-based corpus clustering subcommand and wire it into the CLI, backed by a pure-Rust TLSH implementation.

New Features:

  • Introduce the offsetscan cluster subcommand for TLSH-based similarity hashing and single-linkage clustering across a file corpus, producing per-file records with TLSH digests and deterministic cluster IDs.

Enhancements:

  • Add a dedicated clustering module with deterministic, whole-corpus TLSH clustering semantics and structured warnings for unreadable or TLSH-ineligible files.

Build:

  • Bump crate version to 0.3.0 and add the tlsh2 dependency for TLSH hashing, updating the package description to mention similarity clustering.

Documentation:

  • Document the new offsetscan cluster command, its threshold semantics, output shape, and operational characteristics in the README, and record the 0.3.0 release and planned offsetscan rich feature in the changelog.

Tests:

  • Add unit tests covering TLSH digest determinism, clustering behavior (including near-duplicates and order-independence), error handling, and JSON field naming for clustering output.

@sourcery-ai

sourcery-ai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements a new offsetscan cluster subcommand that performs TLSH-based similarity hashing and single-linkage clustering across a corpus, wiring it into the CLI, documenting its behavior, and introducing a pure-Rust TLSH dependency for version 0.3.0.

Flow diagram for offsetscan cluster TLSH similarity clustering pipeline

flowchart TD
  A[CLI Commands::Cluster] --> B[main expand_paths]
  B --> C[cluster_run]
  C --> D[Read files in parallel std_fs_read]
  D --> E[cluster_reads]
  E --> F[Compute SHA256 sha256_hex]
  F --> G[Build TLSH digest TlshDefaultBuilder]
  G --> H{TLSH digest present?}
  H -- Yes --> I[Store ClusterRecord with tlsh]
  H -- No --> J[Store ClusterRecord with Tlsh null and warning]
  I --> K[Pairwise TLSH diff and edge discovery]
  J --> K
  K --> L[UnionFind union connected indices]
  L --> M[Group components with size >= 2]
  M --> N[Deterministically assign ClusterId]
  N --> O[Emit JSON or NDJSON in main]

  subgraph cluster_rs
    C
    E
    F
    G
    K
    L
    M
    N
  end

  subgraph union_find
    L
  end
Loading

File-Level Changes

Change Details Files
Add TLSH-based clustering engine and data model for corpus similarity analysis.
  • Introduce cluster.rs module implementing TLSH hashing, pairwise distance computation, and single-linkage clustering over a set of files.
  • Define ClusterRecord with PascalCase/OffsetInspect-style fields (File, FileSize, SHA256, Tlsh, ClusterId, Warnings) and serde attributes for JSON output.
  • Implement parallel file reading and hashing using rayon, including per-file warning handling for read failures and TLSH-null cases instead of aborting the run.
  • Use a custom union-find structure to group files into clusters based on TLSH distance threshold, skipping entries without digests and assigning deterministic 1-based cluster IDs ordered by smallest member path.
  • Add deterministic TLSH behavior, clustering logic, error/warning handling, and JSON field-name parity tests in cluster_reads unit tests.
src/cluster.rs
Wire the new clustering functionality into the CLI and output pipeline.
  • Add cluster module import and a new Cluster variant to the Commands enum with path, recurse, and threshold arguments (default 70).
  • In main, expand input paths, call cluster::run, and emit either a JSON array or NDJSON lines, mirroring existing subcommand output handling but without CSV support.
  • Document cluster as a non-streaming, whole-corpus operation that still supports --ndjson but not --csv.
src/main.rs
Introduce TLSH dependency and bump crate metadata for the new feature release.
  • Add tlsh2 dependency with the diff feature enabled, documenting that it is pure Rust and requires no system toolchain.
  • Update crate version from 0.2.3 to 0.3.0 and extend the package description to mention TLSH similarity clustering.
  • Regenerate Cargo.lock to include the new dependency graph.
Cargo.toml
Cargo.lock
Document the TLSH clustering feature, release notes, and CLI usage.
  • Extend README to describe offsetscan cluster, its TLSH basis, clustering semantics, JSON output shape, determinism guarantees, limitations (O(n^2), corpus-scale, no CSV), and example usage.
  • Clarify that offsetscan cluster intentionally sits outside the OffsetInspect JSON parity contract and is OffsetScan-only output.
  • Add a 0.3.0 entry to CHANGELOG describing the TLSH clustering feature, behavior, implementation details, dependency choice, and operational notes, plus an Unreleased section mentioning a future offsetscan rich subcommand.
README.md
CHANGELOG.md

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

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 issue, and left some high level feedback:

  • Consider validating/clamping the threshold argument in Commands::Cluster/cluster::run (e.g., rejecting negative values or values above the maximum meaningful TLSH distance) so obviously invalid inputs don’t produce surprising clustering behavior.
  • The run/cluster_reads path currently reads each file fully into memory before hashing; if you anticipate very large files or corpora, it may be worth switching SHA-256 and TLSH to streaming over Read to bound peak memory per file.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider validating/clamping the `threshold` argument in `Commands::Cluster`/`cluster::run` (e.g., rejecting negative values or values above the maximum meaningful TLSH distance) so obviously invalid inputs don’t produce surprising clustering behavior.
- The `run`/`cluster_reads` path currently reads each file fully into memory before hashing; if you anticipate very large files or corpora, it may be worth switching SHA-256 and TLSH to streaming over `Read` to bound peak memory per file.

## Individual Comments

### Comment 1
<location path="README.md" line_range="197" />
<code_context>
+]
+```
+
+`ClusterId` is shared across near-duplicates and `null` for a singleton. Files too small or too
+uniform for TLSH (and unreadable files) come back with `Tlsh: null` and a `Warnings` entry
+rather than failing the run. Digests carry the `T1` version prefix, so they compare directly
</code_context>
<issue_to_address>
**nitpick (typo):** Consider adding "is" before `null` for smoother grammar.

This minor tweak improves the sentence flow while keeping the meaning unchanged.

```suggestion
`ClusterId` is shared across near-duplicates and is `null` for a singleton. Files too small or too
```
</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 README.md
]
```

`ClusterId` is shared across near-duplicates and `null` for a singleton. Files too small or too

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (typo): Consider adding "is" before null for smoother grammar.

This minor tweak improves the sentence flow while keeping the meaning unchanged.

Suggested change
`ClusterId` is shared across near-duplicates and `null` for a singleton. Files too small or too
`ClusterId` is shared across near-duplicates and is `null` for a singleton. Files too small or too

@warpedatom
warpedatom merged commit ded6ae2 into main Jul 27, 2026
6 checks passed
@warpedatom
warpedatom deleted the feat/tlsh-cluster-0.3.0 branch July 27, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant