HN user

rkochanowski

45 karma
Posts1
Comments24
View on HN

Thanks, I will look at this in more detail.

To give more context, the current version is already an optimized one I considered good enough and didn't spend more time on it. In the first attempt, I used a vector database with indexes, trying to query for similar vectors. This was uselessly slow even in medium-sized repos. The brute-force NumPy solution is a significant improvement, making it faster than other calculations like clustering.

"vectors are not loaded into memory at once" is not true, I had in mind splitting computation into blocks.

One possible simple optimization is to use 16-bit floats in vector instead of 32-bit. I used this in a different project (halfvec in pgvector) without affecting results.

1536-dim embeddings from your case also can be reduced. This large vector usually doesn't give much benefit compared to smaller ones. And this is something I will compare in my own tests.

"Non-exact" may be interpreted in different ways, so let's look at the example report: https://github.com/rafal-qa/slopo/tree/main/doc/example-repo...

* cluster-01.md has the highest similarity, and jscpd probably will detect this too.

* cluster-10.md has the lowest similarity still above the threshold, and I don't think jscpd will detect this as clones. Because they are not clones, this is a false positive that needs to be discarded. But in other cases this kind of similar code may be worth acting on.

I didn't compare with jscpd because I don't consider it a competition. Embedding-based duplication detection works differently, gives different results and has its own trade-offs.

Slopo doesn't deduplicate. It only reports similar code units, which in most cases are not duplicates. This needs to be cleaned up by the user, which is fast and easy with coding agents.

Embedding calculation is also outsourced externally: it's only a simple API call with a LiteLLM wrapper.

"brute-force similarity search" and O(n^2) may sound scary, but it works fine and this is not a bottleneck. For large projects, other parts are much slower, which has room for improvement. [1] is an implementation you probably saw. It uses NumPy, spreads work across all CPU cores and there is also a split into blocks (block_size = 1000). In larger sets, all vectors are not loaded into memory at once. Where I need to be honest, I didn't measure actual memory usage. I just tested this on large repos, so I'm aware of bottlenecks.

From my perspective, this is the easiest part. Code extraction, chunking, applying boost, clustering, generating report and designing everything as a single user-friendly tool is a real challenge. Architectural and product decisions are more difficult than implementation and solving performance issues.

[1] https://github.com/rafal-qa/slopo/blob/v0.3.0/src/slopo/anal...

Finding similar code is something different than deduplication, even when the final goal looks similar.

Deduplication backend is the easiest part of the tool and it doesn't need any additional libraries. Just calculate embeddings and find close pairs. The complexity is everything around.

Using local models is worth considering and the tool already uses the LiteLLM wrapper, allowing it to configure different models, including local. I left this part for the user.

My solution for false positives is simpler:

1. The tool uses only cosine similarity plus boost depending on distance in the codebase.

2. Classification with LLM. This can be done by coding agent used with project giving better results than integrating this pass in the tool. LLMs used for coding are pretty good.

I assumed that this is not a problem I need to solve inside the tool. I'm aware this is not deterministic, but this is by design.

Regarding information about raw similarity: currently, the score (raw similarity + boost) is visible in the report, so this value can be configured based on data. The raw similarity threshold can also be configured, but it's not displayed. I will think about how to handle this.

Currently, only whole functions (including function-like constructs depending on language) are considered as unit.

Skipping the extraction of conditional branches was my decision to not overcomplicate the first versions, which was intended to validate the idea. I will add this in future versions because I agree it's needed for large functions.

I don't think it needs configurable granularity. In the current version, there is an analogous mechanism: when functions are nested, both outer and inner are embedded separately. When both are similar to each other, this pair is excluded. Inner or outer functions can appear in results depending on similarity to other units.

Regarding comments, they are removed and I will think about handling them. The challenge is not with extraction, but with how to present this in a report. This may be a nice addition because coding agents often add comments.

Thanks for the feedback.

Generally, I chunk by function/method (not by whole class), but different languages have specific concepts and features. Nested code units, anonymous functions, lambdas, closures are extracted as separate chunks.

The chunk size has allowed range and those outside are simply ignored.

- Upper limit is hardcoded with a body size of 10k chars

- Lower limit is configurable with a default of 10 AST nodes inside the body

The chunking strategy is something that can be improved in future versions.

Recently there was a popular article on HN saying that sometimes code duplication is better than abstraction, so I assume that this question is not a joke.

While testing this tool, one detected duplication was interesting for a use case. Permission check logic was duplicated and placed in different distant places in the codebase. The code was similar, but not identical, the logic was not the same. One version had stricter checks. I analyzed this with the coding agent, and we found out that both versions are used for the same thing, which means that in some cases validation is insufficient. Having only a single validation place, this bug could be prevented or easily detected.

No. It depends the most on general code quality and architecture. Some implementations require more code similarity by design. Some languages, like Java, may tend to have more duplication, but it's only a theoretical guess. It also depends on what kind of software is developed with what language.

If you are interested in data, you can check my article. Analysis was done with this tool, but a previous version where exact-copy duplicates were excluded from analysis. https://rkochanowski.com/article/analysis-code-duplication/

I built Slopo to solve one specific problem: finding similar code that is hardest to detect by other tools, coding AI agents, and humans.

It finds similar-looking code with embeddings. This detects more than just copy-paste clones or even clones with minor changes. Similar code is often not a clone to refactor, and this is a trade-off. Initial results need to be verified, but coding agents can do this quickly. Example prompts are available on https://slopo.dev

Additionally, similar code distant in the codebase is ranked higher to focus on less obvious duplication.

The results differ a lot depending on the codebase. I noticed that sometimes most of the detected duplicates are false positives, but the remaining ones are strong candidates to refactor or even bugs. Sometimes it reveals much more real duplication.

That's why it's so important to design a solution before letting AI implement it. I noticed that AI often can't see clean, elegant and primitive solutions that fit best for a given problem. Even when I ask if it can be designed in a simpler way, it can't see what I can see. I think this is one of the most important points where humans should be involved.

There are bunch of tools to manage context or fix what Claude does wrong. They may be popular because non software engineers want to improve their workflows, like you mentioned.

But are they really working instead of making it worse? Are there any tests or real case studies done by users not tool's author? From my experience, removing from context works more often then adding.

I realized how easily it can be confused and generate a bad answer in effect. It's difficult to control context when we have instructions in AGENTS/CLAUDE.md, memory, SKILLs, sometimes additional tools for optimization. On top of this our prompt. When coding agent misbehaves, there is often some issue on my side that steered it in a wrong direction. It requires more precision than I initially thought.

I think "lack of effort required" is the key.

We can just throw prompts without understanding and caring about output. This way we can build something in 10 min we don't own.

We can also do real engineering with our architecture design, thinking about what exactly are we building and why, being critical about produced output. This way of building takes more time, but this is something we built ourselves and can be potentially more fulfilling. And this is not something anyone can copy with right prompt. There are more nuances than AI-generated code.

Another way to find fulfillment is to focus more on solving a real problem and building something people will use. Less on writing a code.