# Rule Authoring

## Identity

Every Astrale-specific domain rule has one canonical semantic identifier:

```text
astrale/<slug>
```

Examples:

```text
astrale/core-is-pure
astrale/effects-run-in-step
astrale/no-read-inside-mutate
```

The `astrale` namespace is declared once by the linter. The registry stores only the slug. Slugs use
lower-case kebab case, state the invariant clearly, and avoid engine words such as `oxlint`, `ast`, or
`typescript`.

Prefer established lint vocabulary:

- `no-*` prohibits a concrete pattern.
- `require-*` requires explicit syntax or evidence.
- `prefer-*` is advisory when valid exceptions exist.
- A direct invariant such as `core-is-pure` is acceptable when it reads more clearly.

Artificial abbreviations, trigrams, category prefixes, and monotonic numbers are not identifiers.
Family and owner can change without renaming a rule.

## Stability

Identifier stability begins at `accepted`, not `proposed`:

- A `proposed` rule may be renamed, merged, or removed freely.
- An `accepted` or later rule keeps its canonical identifier.
- If an accepted rule must be renamed, its old identifier remains a deprecated alias through at least
  the next major SDK release.
- A retired identifier is never assigned to a different invariant.

This preserves early design freedom without maintaining an opaque surrogate code alongside every
readable rule name.

## Classification

Every registry entry declares:

| Field | Values |
| --- | --- |
| `family` | `structure`, `purity`, `schema`, `authorization`, `durability`, `graph`, `errors`, or `client` |
| `owner` | `sdk`, `oxlint`, or `analyzer` |
| `scope` | `node`, `file`, or `project` |
| `automation` | `implement`, `research`, or `guidance` |
| `default` | `error`, `warning`, or `off` |
| `fix` | `safe`, `suggestion`, or `none` |
| `status` | `proposed`, `accepted`, `implemented`, `enforced`, `deprecated`, or `retired` |

Classification is metadata, not identity. Moving `astrale/authorize-is-explicit` from an analyzer hint
to an SDK type invariant preserves its identifier, suppressions, documentation URL, and history.

`automation` is an explicit decision, not a maturity score:

- `implement` means the current contract is suitable for automation and may enter the rule lifecycle.
- `research` means the idea is not approved for implementation. Its row must state what evidence or
  narrower contract is missing.
- `guidance` means the current formulation should remain knowledge or review guidance. Its row remains
  in the registry as a decision record, with `default: off` and no fix.

Changing `research` or `guidance` to `implement` requires a review of the detection contract and
false-positive fixtures. It is never implied merely because an engine could approximate the rule.

## Minimal policy specification

Every entry in [RULES.md](./RULES.md) records:

- A unique semantic slug.
- Family, primary owner, scope, automation disposition, intended default severity, fix class, and
  status.
- One-sentence invariant.
- For `research`, the evidence or narrower contract needed before implementation.
- For `guidance`, why static enforcement would be misleading.

Before an `implement` entry can become `accepted`, it additionally requires:

- A deterministic detection contract and relevant source locations.
- At least one valid and invalid fixture.
- Representative legitimate alternatives and documented exceptions that must not trigger.

An accepted rule should graduate to a dedicated document only when examples, options, or edge cases no
longer fit clearly in the registry. Its filename is `<slug>.md`, for example
`effects-run-in-step.md`.

## Lifecycle

```text
proposed -> accepted -> implemented -> enforced
                          |              |
                          +-> deprecated +-> deprecated -> retired
```

- `proposed`: candidate policy; no compatibility guarantee and no default diagnostic.
- `accepted`: identity, detection contract, and ownership approved.
- `implemented`: rule exists behind an explicit experimental preset.
- `enforced`: enabled at its documented severity in the strict preset.
- `deprecated`: still recognized, with replacement guidance where applicable.
- `retired`: no diagnostic is emitted; its historical identifier remains reserved.

No rule moves directly from prose to `enforced`.

Only entries classified `implement` may move from `proposed` to `accepted`. A `research` entry may have
an isolated prototype for measurement, but that prototype must not emit default project diagnostics.
A `guidance` entry does not enter the executable lifecycle unless it is narrowed and reclassified.

## Severity policy

- `error` means the code is structurally unsafe, invalid, or violates a settled Astrale contract.
- `warning` means the observation is reliable but the observed pattern is risky rather than invalid in
  every occurrence.
- `off` means the rule is experimental, expensive, or awaiting platform support.

Severity is not a proxy for implementation progress. An unimplemented rule remains `proposed` even if
its intended default is `error`.

Warnings are not a shelter for noisy heuristics. A likely false positive still consumes attention,
creates suppressions, and weakens trust in every other diagnostic.

## Owner migration

Enforcement moves to the earliest reliable owner. A source rule should migrate from Oxlint or the
analyzer to `sdk` once an SDK type, schema compilation path, or runtime authoring contract can make the
invalid shape impossible. During migration, one owner emits the diagnostic; dual reporting is a bug.

## Rule test contract

Every implemented rule includes:

- Minimal valid fixtures.
- Minimal invalid fixtures with exact identifier, location, and message.
- False-positive fixtures for documented exceptions.
- Suppression fixtures.
- Fix snapshots when `fix` is `safe` or `suggestion`.
- A performance fixture when the rule walks imports, types, or compiled schema.
