# Similar issues and grouping suggestions

Bucker groups events into issues by fingerprint. Two issues that are really one bug (the same
failure reached by a slightly different stack, or reworded by a different library version) are
found a second way: by comparing what the issues say. The result appears in three places:

- **Similar issues** on an issue page, with any fix recorded for the neighbours.
- **Grouping suggestions**: pairs of issues that look like the same bug, waiting in the
  project's review queue (Grouping, Review queue).
- **Merge**, in the queue and in the issue stream, to fold the duplicates together.

Nothing is ever merged because it looked similar. A suggestion is a question for a person. The
only automatic merging in the product is the opt-in auto-merge policy, which stays off until an
organization's own confirmed and rejected pairs show it is accurate enough (the precision gate
on the same screen).

## What is compared

Each issue is turned into a 768-number vector from its exception type and message, its title and
its culprit. Nothing else: not event payloads, not user data, not breadcrumbs, not source code.
Two issues are close when their vectors are.

Two providers can make the vectors.

| Provider | What it understands | Needs |
| --- | --- | --- |
| Local (default) | Shared words, identifiers and character runs. `getUserById` and `findUserById` match; two descriptions of the same failure in different words do not | nothing |
| Hosted | Meaning, so a message and its rewording are close | an API key, and a person to get one |

The local provider is deterministic and offline, which is why it is the default and the
fallback. The hosted one speaks the OpenAI-compatible `POST /v1/embeddings` shape, so it works
with OpenAI, Azure OpenAI, Voyage, Mistral, Together, vLLM and Ollama.

## Turning the hosted provider on

This is a step for a person with an account at the provider. Bucker cannot get a key.

1. Create an API key with the provider.
2. On the API, worker **and MCP** services set `EMBEDDING_PROVIDER=hosted` and `EMBEDDINGS_API_KEY`,
   with identical values: every neighbour query filters on the model name, so a service that
   resolves a different provider silently finds nothing.
   Optional: `EMBEDDINGS_API_URL` (default `https://api.openai.com/v1/embeddings`) and
   `EMBEDDINGS_MODEL` (default `text-embedding-3-small`). The model must honour the
   `dimensions` request field and return 768; a model that cannot is refused, never padded.
3. Deploy. Vectors made by the old provider are rebuilt automatically, a project at a time,
   because every vector records the model that made it and two models are never compared.

If `EMBEDDING_PROVIDER=hosted` is set and the key is not, Bucker keeps grouping on the local
provider and says so: the worker logs a warning at boot, and a scan reports why it ran on the
local provider. Silently grouping worse is the failure this avoids; refusing to group is worse
still.

### What leaves your deployment

With a hosted provider, an issue's exception type, message, title and culprit are sent to it
when the issue is created and when its vector is rebuilt. Two switches keep things local:

- An organization with **AI features turned off** (its AI switch) always uses the local
  provider, and nothing about its issues leaves the deployment.
- **Source code is never sent** to the hosted provider by the codebase index unless the
  deployment sets `EMBEDDINGS_ALLOW_SOURCE_CODE=true`, a separate decision from grouping.

Bucker's hosted service embeds **issues only**: it leaves `EMBEDDINGS_ALLOW_SOURCE_CODE` unset, so
code retrieval for root-cause analysis and fix context stays on the local provider. Sending source
code too would wait on a per-organization AI-switch check on the codebase path, which does not exist
yet.

If the provider fails while an issue is being embedded (an outage, a revoked key), the embedding
is not stored, and the next nightly pass embeds it; it is not replaced by a local vector, which would put a weaker
vector into a hosted index under the wrong name.

## The nightly pass

A worker pass runs about once a day. For every project with at least two issues seen in the
last week it embeds any issue that has no vector from the current provider (up to 500 per
project per night, busiest projects first, 200 projects per night, so a large backlog drains
over several nights rather than in one bill), and then scans for similar pairs at the usual
threshold. Pairs you already decided are never asked again. `GROUPING_SWEEP_ENABLED=false`
turns the pass off; **Scan for duplicates** in the queue still runs one project on demand.

## Merging suggestions

Tick the suggestions that are the same bug and press **Merge N selected**. Each is confirmed
(so it counts toward the precision statistic) and merged, up to 50 at a time. For each pair the
older issue survives, because it holds the short ID people have already pasted into tickets.

- A pair that cannot be merged (one side was merged elsewhere, deleted, or someone else decided
  it first) is skipped with its reason and stays in the queue.
- Every merge is audited and can be undone from Grouping, Merge history, with the counters
  restored exactly.
- It needs the right to triage issues in the project.

In the issue stream, select two or more issues of one project and choose **Merge** to pick the
survivor yourself.

```
POST /orgs/:org/projects/:project/grouping-suggestions/merge
{ "suggestionIds": ["...", "..."] }
```

The response lists, per suggestion, `merged` or `skipped` with a reason.

## What this is not

Bucker does not train or fine-tune a model, and it does not merge on similarity alone. The
published wrong-merge rate of automatic embedding merges is high enough that the product keeps a
person in the loop until your own data says otherwise.
