> ## Documentation Index
> Fetch the complete documentation index at: https://docs.a7manager.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Recurring Faults Logic

> How a defect is matched against earlier defects on the same asset, and what hides a match.

This page is the decision logic behind the **Recurring faults** tab on the Warranty page.
Read the [tab description](/pages/warranty#recurring-faults) first; this page explains the
order in which the rules are applied.

***

## Matching one pair

The subject is always the **later** defect. Every earlier defect on the same asset within the
window is a candidate, and each candidate is tested in the order below. The first rule that
holds decides the **basis** of the pair; the rules after it are not evaluated.

```mermaid theme={null}
flowchart TD
    S(["Subject defect A"]) --> W{"Candidate B:<br/>same project,<br/>earlier than A,<br/>inside the window?"}
    W -- no --> X0(["no match"])
    W -- yes --> C{"Same component?"}
    C -- yes --> B1(["COMPONENT"])
    C -- no --> V{"Same vehicle?"}
    V -- no --> X1(["no match"])
    V -- yes --> L{"Location set on both<br/>and different?"}
    L -- yes --> X2(["no match"])
    L -- no --> F{"Same fault<br/>indication code?"}
    F -- yes --> B2(["FAULT_CODE"])
    F -- no --> P{"Product groups on<br/>one root path?"}
    P -- yes --> D{"Both nodes<br/>below L1/L2?"}
    D -- yes --> B3(["PRODUCT_GROUP"])
    D -- no --> T{"Text similarity<br/>≥ text threshold?"}
    T -- yes --> B4(["TEXT"])
    T -- no --> X3(["no match"])
    P -- no --> G{"Text similarity<br/>≥ cross-group threshold?"}
    G -- yes --> B5(["CROSS_GROUP"])
    G -- no --> X4(["no match"])
```

Two thresholds appear in the graph and they are not the same control:

| Threshold             | Default | Where it is set                         | Applies to         |
| --------------------- | ------- | --------------------------------------- | ------------------ |
| Text threshold        | `0.4`   | Server setting (`min_score` on the API) | `TEXT` only        |
| Cross-group threshold | `0.7`   | Server setting only                     | `CROSS_GROUP` only |

Text similarity is computed for every pair and shown as a percentage, whatever the basis.
On `COMPONENT`, `FAULT_CODE` and `PRODUCT_GROUP` pairs it is informational and can be low.

The graph reads the product-group rule as a strict order: a pair on one root path never
becomes `CROSS_GROUP`. If its text is below the text threshold but at or above the
cross-group threshold, it still qualifies, and it keeps the `TEXT` basis.

<Note>
  The window is directional. A defect only looks backwards, so a pair is listed once, on the
  later defect. The earlier defect gets no row from that pair, though it may have rows of its
  own from defects that preceded it.
</Note>

***

## From pairs to rows

A grid row is one subject defect with at least one visible pair. The row aggregates its pairs:

* **Component, Fault code, Product group** — a green check when any pair shares that criterion.
* **Text** — the highest similarity among the pairs.
* **Matches** — the number of pairs.

Rows sort by the strongest basis among their pairs, in the order `COMPONENT`, `FAULT_CODE`,
`PRODUCT_GROUP`, `TEXT`, `CROSS_GROUP`, then by descending score. The drawer lists the pairs
in the same order.

***

## What hides a pair

Matching is computed on every request. Two persisted states take pairs out of the default
view. The tab never shows them; the API returns them flagged with the reason when asked
(`include_hidden`).

```mermaid theme={null}
flowchart LR
    M(["Matched pair"]) --> DQ{"Pair dismissed<br/>by a Warranty Manager?"}
    DQ -- yes --> H1(["hidden: DISMISSED"])
    DQ -- no --> HQ{"Subject has a<br/>REPARATION claim?"}
    HQ -- yes --> H2(["hidden: HANDLED"])
    HQ -- no --> VIS(["visible"])
```

| State         | Recorded where                                                                                                                                                                               | Reversible                    |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| **Dismissed** | One row per pair in `similarity_recurrence_dismissal`, stored with the two defect ids in a fixed order so the same pair is never stored twice and hides in both directions, at any threshold | No — only by deleting the row |
| **Handled**   | Derived from warranty data: a claim of type `REPARATION` on the subject defect                                                                                                               | Follows the claim             |

The drawer is where both states are produced. **Dismiss** records one pair, **Dismiss all
matches** records every visible pair of the subject, and **Create warranty claim** raises the
`REPARATION` claim that marks the subject handled. That claim stores one visible match as its
original defect — used directly when there is one, picked by the Warranty Manager when there
are several — so the shortcut is absent once every match is hidden. All three are Warranty
Manager actions.
