# Defence Before Fix: Detector Specification

**Version**: 1.0.0, published 2026-09-08
**Companion to**: [the method specification](SPEC.md), version 1.0.1, and [the toolchain specification](TOOLING-SPEC.md), version 0.2.0
**Author**: [Joseph Edmonds](https://ltscommerce.dev), [Edmonds Commerce](https://edmondscommerce.co.uk)
**Coined**: 22 February 2026, in [the original article](https://ltscommerce.dev/articles/defence-before-fix-static-analysis)

## 1. What this document is for

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT and MAY are to be interpreted as
described in RFC 2119.

[The method specification](SPEC.md) states what a [Practitioner] does when a
[Defect] is found. This document states what a [Detector] must offer so that they
can do it: write a [Rule], prove it, run it and resolve what it prints. It is addressed to
whoever maintains a [Detector], which the method specification defines as a tool that reads
code without executing it and reports occurrences of a pattern.

A [Detector] is one part of a project's [Toolchain], and this document judges it
on what it offers alone. What the assembled [Toolchain] of a project must add on top, the
[Project record], the listing of active [Defences] and the
governance of [Suppression], is stated in [the toolchain specification](TOOLING-SPEC.md).
The two are separable because they are built by different people: a [Detector] is authored once
and installed into projects its maintainer will never see, whilst governance is decided by each
project for itself. A document that asked a [Detector] to enforce a project's governance would
fail every [Detector] in use and would not make any project better governed.

**The governing principle**: where the method specification requires a [Practitioner] to do
something with a [Rule], a [Conforming] [Detector] MUST make that possible
without the project building the mechanism first. A [Detector] that leaves the project to
construct the means has moved the obligation rather than met it.

## 2. Terminology

Terms from the method specification carry over unchanged. Every capitalised term links to its
definition; the ones this document leans on most are, in short:

| Term           | In one line                                                                    |
| -------------- | ------------------------------------------------------------------------------ |
| [Detector]     | A tool that reads code without executing it and reports what it finds          |
| [Rule]         | One check a [Detector] evaluates                                               |
| [Identifier]   | The stable name printed with a finding that leads to its documentation         |
| [Practitioner] | Whoever is doing the work, a person or an [Agent]                              |
| [Toolchain]    | Everything a project assembles to run its checks through, [Detectors] included |
| [Suppression]  | Making a finding go away without fixing it                                     |
| [Baseline]     | A recorded set of existing findings a [Rule] is told to ignore                 |

These are additional.

#### Rule author

Whoever writes a [Rule]. May be the [Detector]'s maintainer, a [Toolchain]'s maintainer or the project that runs it.

#### Bundled rule

A [Rule] the [Detector] ships, or fetches on the [Practitioner]'s behalf from a source the [Detector]'s maintainer controls. Its [Rule author] will never see the codebases it runs in, so everything a [Practitioner] needs in order to act on it has to travel with it.

#### Harness

The route by which one [Rule] is run against supplied code and its findings observed, without the project's own test suite and without every other [Rule] running alongside it.

The distinction between a [Bundled rule] and a project's own [Rule] matters throughout.
The [Detector] owns the documentation of the first and can be held to shipping it; it cannot
know the documentation of the second, and what it owes there is the mechanism that lets the project
attach its own.

## 3. The division of responsibility

Each mechanism the method needs has two halves. Stating only one of them is what produces a
[Detector] that is *nearly* usable.

| Requirement                          | The [Detector] MUST provide                             | The [Rule author] supplies                              |
| ------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------- |
| Bespoke [Rules]                      | The authoring and registration route                    | The [Rules]                                             |
| The red proof                        | A [Harness]                                             | The [Fixture] and the red run                           |
| Running the [Rule]                   | A local invocation, including subsets                   | Running it                                              |
| The [Identifier]                     | A place to carry it, and printing it with every finding | Choosing it once                                        |
| Resolution of a [Bundled rule]       | The lookup, and the documentation, on disk              | Nothing                                                 |
| Resolution of a project's own [Rule] | The [Identifier] printed unaltered                      | The documentation and, with the [Toolchain], the lookup |
| Inline [Suppression]                 | A route the project can detect or disable               | The decision whether to forbid it                       |

Read down the middle column and the shape of a [Conforming] [Detector] is already
visible. Read across any row and the failure mode is visible too: either half alone leaves the
[Practitioner] stuck.

## 4. Authoring rules

### 4.1 The detector MUST support bespoke rules written by the project that runs it

Configuration of an existing [Rule] set is not sufficient. The project must be able to express a
pattern the [Detector]'s authors have never anticipated, and register it so that it runs with
the same standing as a [Bundled rule].

**Why**: the method turns a specific [Defect] into a [Class] [Defence], and the
[Classes] that matter most to a project are the ones peculiar to it. A [Detector] offering
only a fixed catalogue can defend against the industry's known [Hazards] and none of the
project's own, which is the half that carries its institutional knowledge.

### 4.2 The detector MUST provide a harness that runs a single rule against supplied code

A [Harness] reports, for a given input, whether the [Rule] fired and what it printed, without
executing the project's own test suite and without the other [Rules] obscuring the answer.
This applies to every [Rule] the [Detector] runs, bundled or bespoke; a
[Detector] failing 4.1 is graded here on its [Bundled rules].

**Why**: clause 3.3 of the method specification requires the [Rule] to be proven by making it go
red. A [Practitioner] who can only observe a [Rule]'s behaviour by running every
[Rule] over the whole codebase cannot demonstrate that a [Rule] fires on the pattern rather
than on something incidental.

### 4.3 The detector MUST allow a rule to carry a stable identifier, and MUST print it with every finding

The [Identifier] MUST be stable across releases and MUST NOT be derived from the [Rule]'s
file path, [Class] name or position in a configuration file. The [Rule author] chooses it
once. The [Detector] MUST print it, unaltered, alongside every finding the [Rule]
reports, in its default output and in every machine-readable format it offers. A prefix the
[Detector] adds from the invoking directory or the [Rule]'s location is derived from
the file path. A namespace the [Rule author] assigns once in configuration is not.

| [Identifier]       | Where it comes from                                                         | Stable? |
| ------------------ | --------------------------------------------------------------------------- | ------- |
| `proj.no-raw-sql`  | Written once by the [Rule author] in configuration, printed as is           | Yes     |
| `LM-0107`          | Assigned once by the [Detector]'s maintainer at the [Rule]'s first release  | Yes     |
| `rules/no-raw-sql` | Produced from where the [Rule]'s file sits; changes when the file moves     | No      |
| `NoRawSqlRule`     | The [Rule]'s [Class] name; changes when the [Rule] is renamed               | No      |
| `rule 7`           | The [Rule]'s position in a configuration file; changes when one is inserted | No      |

**Why**: the [Identifier] is the only string that reaches the [Practitioner] and the
only key their lookup can use. An [Identifier] that changes when a [Rule] is renamed
silently breaks every reference to it, including references written down by people who have left.
An [Identifier] the [Detector] carries but does not print is one the
[Practitioner] was never given.

### 4.4 The detector SHOULD enforce 4.3 with a rule of its own

A [Rule] over the [Rules], failing any [Rule] that reports without a stable
[Identifier].

**Why**: this is the method applied to the [Detector] itself, and it is cheap. The worked
example is `php-qa-ci`'s `RequireRuleIdentifierConstantRule`, a [Rule] hosted in PHPStan by a
[Toolchain] built on it: it rejects a magic-string [Identifier] and names the
constant to declare instead. Nothing about it needed to live outside the [Detector].

## 5. Reporting

### 5.1 The detector MUST be invocable by the practitioner, locally, with no infrastructure

**Why**: method specification clause 8.1. An [Agent] that cannot check its own work cannot
iterate against a [Defence], so the loop never closes in the turn where it is cheap to close.

### 5.2 The detector MUST support invocation over a subset, at minimum a single file

**Why**: 5.1 is satisfied in principle by a whole-codebase run and defeated in practice by one.
Checking a single edited file has to be fast enough to do on every edit, or it will not be done on
any.

### 5.3 The result MUST reach the practitioner in the output of the command they ran

Where the [Detector] writes fuller detail elsewhere, the invoked command's own output MUST
carry both a usable summary and the location of the remainder.

**Why**: method specification clause 8.2. A report the [Practitioner] has to go and find is a
report that arrives after the decision it was meant to inform.

### 5.4 A finding MUST NOT be reportable only through a hosted service, licence tier or CI-only mode the practitioner cannot invoke locally

A finding that the [Detector] reports only through a hosted service, a licence tier or a
continuous-integration-only mode the [Practitioner] cannot invoke locally does not
[Conform], whatever the same [Rule] reports there.

**Why**: a [Rule] that fires only in an environment the [Practitioner] has no access to
teaches nobody anything and blocks them anyway, which is the worst combination available.

## 6. Resolving an identifier

### 6.1 The detector MUST provide a mechanism that resolves a printed identifier to its documentation

Two cases, by who wrote the [Rule]:

| The [Rule]        | Who supplies the documentation | What this clause asks of the [Detector]                                   |
| ----------------- | ------------------------------ | ------------------------------------------------------------------------- |
| A [Bundled rule]  | The [Detector]                 | Resolve the [Identifier], presented alone, to that documentation          |
| The project's own | The project                    | Print the [Identifier] unaltered; the lookup is the [Toolchain]'s to give |

In both, the mechanism is keyed on **the [Identifier] exactly as printed**. A command, an index
file or a URL are all acceptable forms. Each case in turn:

**For a [Bundled rule]**, the [Detector] supplies the documentation and the
lookup, and clauses 6.2 and 6.3 say where. The mechanism MUST resolve an [Identifier]
presented alone, because the reader who needs it most has the [Identifier] from a log, a
ticket or a colleague and not the [Message]. A [Message] that carries its own
documentation path resolves that [Message], not the [Identifier].

**For a project's own [Rule]**, the [Detector] cannot know the documentation, so what
it owes is the half it can give: the [Identifier] printed unaltered under clause 4.3, and no
transformation of it. Where the [Identifier] is itself a URL, as method clause 3.6 allows,
the [Detector] prints that URL and has done what this clause asks. What this clause forbids is
a documentation path printed alongside a shorter [Identifier] that cannot be looked up on its
own. The lookup for such
a [Rule] is an obligation on the project's assembled [Toolchain], under clause 4.2
of the [toolchain specification](TOOLING-SPEC.md), and a [Detector] that offers it as well has
gone further than this clause asks.

**Why**: method specification clause 8.3 requires the [Identifier] to resolve without a human.
An index keyed on anything else does not resolve it. This is the most commonly failed clause in this
document and it fails in a specific way: documentation exists, is genuinely good, and is keyed on the
[Rule]'s [Class] or file name, which is a string the [Practitioner] was never
given. The lookup they can actually perform is the only one that counts.

### 6.2 Resolution of a bundled rule's identifier MUST work from the installed copy, without network access

**Why**: an [Agent] working offline, behind a proxy, or against a URL that has since moved needs
the answer to be on disk. A dependency the project already installed is on disk by definition. A
catalogue on the [Detector]'s website, however complete, is the right documentation in the wrong
place.

### 6.3 A bundled rule's documentation MUST ship with the rule, at a version tracked together

Every [Identifier] a [Bundled rule] can print MUST resolve, under clause 6.1,
to a page in that shipped documentation. One [Identifier] that does not resolve fails this
clause, however many others do. A page per [Identifier] satisfies this; a page per family of
[Identifiers] MAY be used instead, under clause 6.5.

**Why**: method specification clause 3.6. A [Bundled rule] travels into codebases its author
will never see. If its documentation lives only in the [Detector]'s repository or on its
website, then every project that installs it is one link rot away from a [Rule] that blocks
without explaining.

The failure to guard against is not the absent document but the **dangling one**: a reference to
documentation that was planned and never written is worse than no reference, because it consumes
the [Practitioner]'s attention before failing them.

### 6.4 The detector SHOULD fail its own release if a bundled rule lacks resolvable documentation

An automated check over every [Identifier] a [Bundled rule] can print, applying the
family list or pattern of clause 6.5 where one is used, that blocks the release when any of them
lands on no page.

**Why**: clause 6.1 is the clause most easily believed to be satisfied whilst being broken, because
the documentation is written by the same person who wrote the [Rule] and its absence is
invisible from the inside. Where the [Detector] is shipped inside a [Toolchain],
the [toolchain specification](TOOLING-SPEC.md)'s self-audit makes this check a MUST for the [Toolchain]; a
[Detector] released on its own is asked for it as a SHOULD because the same [Class] of
[Defect], a [Rule] that blocks without explaining, is detectable mechanically there too.

### 6.5 One page MAY document a family of identifiers, and SHOULD name its members so a check can confirm them

Where several [Bundled rules] share a prefix, one page MAY document them all. Such a page
SHOULD let the check clause 6.4 asks for confirm that each member resolves to it. Two forms do
that and one does not:

| The page                                                                                                  | A check can confirm each member?          |
| --------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| Lists every full [Identifier] in the family, `LM-0100`, `LM-0101`, and so on                              | Yes                                       |
| Carries a regular expression the check can run, `^LM-01[0-9]{2}$`, matching every member and nothing else | Yes                                       |
| Says in prose that everything under `LM-01` is documented here                                            | No: a person can infer it, a check cannot |

The list or the regular expression lives in the installed file, since clause 6.2 requires
resolution to work from the installed copy, not only in a rendered web page.

**Why**: a family page is a convenience for the author, and the cost of it must not fall on the
check. A page that a person can see covers `LM-0107` but a check cannot leaves clause 6.4 with
nothing to run.

## 7. Suppression

### 7.1 A detector MAY offer an inline suppression route, but MUST make it detectable or disableable

An inline ignore comment, a per-line directive and a generated [Baseline] are all such
routes. The [Detector] MAY ship them. For each one it MUST do at least one of two things, and either
alone satisfies this clause: make the route disableable by configuration; or make the route
detectable, by a [Rule] the project can write in the [Detector] itself or by a mechanical check
the [Detector] documents. Either lets a project which decides to forbid the route enforce that
decision. A route that can be neither switched off nor seen does not [Conform].

**Why**: the method specification's position is that [Suppression] is an [Owner]
decision under its clause 3.4 and section 4, and an [Owner] cannot decide something they are
never shown. The [Detector] is not the [Owner] and does not know the project's
governance, so it is not asked to enforce it; what it is asked is not to hide the route. Every
[Detector] in wide use ships an inline ignore, and a clause that forbade them would fail all of
them without governing any project better. Whether the route is forbidden is the project's decision,
enforced through its [Toolchain] under clause 4.3 of the [toolchain specification](TOOLING-SPEC.md).

### 7.2 An inline suppression route SHOULD require a written reason

The [Detector] SHOULD reject, or be configurable to reject, an inline [Suppression]
that carries no reason, and SHOULD NOT supply a default one, including each entry of a generated
[Baseline].

**Why**: a [Suppression] without a reason is indistinguishable from one nobody would defend,
and the person who could tell them apart is usually gone. Requiring the sentence costs the author a
minute at the moment they have the reason in mind, and it is the only thing that makes the
[Suppression] reviewable later. PHPStan's `reportIgnoresWithoutComments` is the shape of it.

## 8. Conformance

**A [Detector] [Conforms]** if it satisfies every MUST in sections 4 to 7.

Partial [Conformance] MUST NOT be described as [Conformance]. A [Detector]
that satisfies most of this document is in a normal and respectable condition; it is not
[Conforming], and describing it as such removes the only value the word has.

This document defines one level. The clauses of the method specification's section 8 that reach
beyond what sections 4 to 7 here already secure, the listing of active [Defences] and the
summary for an [Agent]'s context, are obligations on a project's assembled [Toolchain]
and are stated in the [toolchain specification](TOOLING-SPEC.md), so there is no separate [Agent]-support level
for a [Detector].

### 8.1 A maintainer MAY declare the version of this document the detector conforms to, and the declaration records known gaps

The declaration is machine-readable, in whatever form the [Detector]'s ecosystem uses to
record dependencies. The same declaration is where a gap against this document is recorded once it is
known: a [Detector] that has learnt, from its own checks or from a [Practitioner]'s
report under the method's clause 3.2, that it fails a MUST in sections 4 to 7 MUST record that gap
alongside the version it declares, in the same file or one it names. A declaration with a non-empty
gap record is a statement of where the [Detector] stands and is not a claim of
[Conformance].

The declaration is optional. It is how a maintainer claims [Conformance]; it is not a
condition of it. A [Detector] that predates this document, or whose maintainer has never read
it, MAY be graded [Conforming] on evidence by anyone who exercises the clauses above against
it, and a verdict on any [Detector], declared or not, rests on that exercise and not on the
claim. A declaration tells the reader what the maintainer believes and what they know to be missing;
the reader still runs the [Harness].

The shape is illustrative rather than prescribed. In a Composer manifest:

```json
"extra": {
  "defence-before-fix": {
    "method": "1.0.1",
    "detector": "1.0.0",
    "toolchain": null,
    "known-gaps": []
  }
}
```

In a package.json, the equivalent is a top-level `defenceBeforeFix` object with the same keys. A
[Detector] that is not also shipped as a [Toolchain] leaves `toolchain` empty;
a project that ships both declares both. Each entry in `known-gaps` names the clause and states the
gap in a sentence.

**Why**: a [Conformance] claim in a README is a sentence; a claim in a manifest is a fact about
a specific installed artefact, checkable by anyone, including mechanically, and it fixes what
"[Conforming]" meant at the point the claim was made. Making the claim a condition of
[Conformance], though, would mean a [Detector] with every mechanism in place could
never [Conform] until its maintainer had heard of this document, which grades the
maintainer's reading rather than the [Detector].

## 9. Relationship to the other specifications

This document adds no obligations to a [Practitioner] and relaxes none. Every clause here
exists to make a clause of the method specification achievable with a [Detector] in hand.

Where this document and the method specification disagree, the method specification governs. It
describes the method, which is the thing being specified; this describes one piece of the equipment.

The [toolchain specification](TOOLING-SPEC.md) states what a project's assembled [Toolchain] must offer beyond
what each [Detector] in it offers, and it requires every [Detector] a [Defence]
is routed through to [Conform] to this one. A [Detector] shipped inside a
[Toolchain] is measured here as a [Detector] and there as part of the
[Toolchain]; the two verdicts are separate and neither implies the other.

Nothing here requires a project to use a [Conforming] [Detector]. A project can
[Conform] to the method specification on a [Detector] that [Conforms] to
none of this, at the cost of building the missing mechanisms itself. This document exists so that it
does not have to.

<!-- Term link definitions -->

[agent]: SPEC.md#agent
[baseline]: SPEC.md#baseline
[bundled rule]: #bundled-rule
[bundled rules]: #bundled-rule
[class]: SPEC.md#class
[classes]: SPEC.md#class
[conform]: SPEC.md#conform
[conformance]: SPEC.md#conform
[conforming]: SPEC.md#conform
[conforms]: SPEC.md#conform
[defect]: SPEC.md#defect
[defence]: SPEC.md#defence
[defences]: SPEC.md#defence
[detector]: SPEC.md#detector
[detectors]: SPEC.md#detector
[fixture]: SPEC.md#fixture
[harness]: #harness
[hazards]: SPEC.md#hazard
[identifier]: SPEC.md#identifier
[identifiers]: SPEC.md#identifier
[message]: SPEC.md#message
[owner]: SPEC.md#owner
[practitioner]: SPEC.md#practitioner
[project record]: TOOLING-SPEC.md#project-record
[rule]: SPEC.md#rule
[rule author]: #rule-author
[rules]: SPEC.md#rule
[suppression]: SPEC.md#suppression
[toolchain]: SPEC.md#toolchain
