# Qualifier Specification

**Version:** 0.4.0
**Status:** Draft
**Authors:** Alex Kesling

---

## Abstract

Qualifier is a deterministic system for recording, propagating, and querying
typed metadata records against software artifacts. It provides a VCS-friendly
file format (`.qual`), a Rust library (`libqualifier`), and a CLI binary
(`qualifier`) that together enable humans and agents to annotate code with
structured quality signals without waiting for a formal process. Each
annotation carries a `kind` (concern, comment, suggestion, pass, fail,
blocker, praise, waiver, resolve, or any custom string), letting tools
filter, thread, and aggregate however they need. Records thread, persist,
and compose through a single content-addressed model.

Records use the [Metabox](METABOX.md) envelope format: a fixed envelope
(`metabox`, `type`, `subject`, `issuer`, `issuer_type`, `created_at`, `id`)
wrapping a type-specific `body` object. Records are content-addressed, append-only, and
human-writable. No server, no database, no PKI required.

## 0. Why Qualifier

Software is full of structured observations that have no good home. A reviewer
notices that a function panics on malformed input. A scanner reports a CVE in a
transitive dependency. A profiler measures a regression on a hot path. A
licensing audit confirms that a vendored file is MIT. Today, each of these
observations lands in a different system — a PR comment, a SARIF report, a
spreadsheet, a wiki page — and none of those systems talks to the others. The
observations decay because they live somewhere code does not. Structured
knowledge about code deserves the same rigor we apply to the code itself.

Consider the alternatives we currently reach for. **GitHub PR comments** are
tied to a diff window and disappear from view the moment the PR merges; the URL
still resolves, but nothing in the working tree points at it, no tool can query
it, and a refactor that touches the same lines a year later has no idea the
conversation ever happened. **SARIF reports** are produced once by a tool, then
either ignored or archived; they have no notion of human reply, threading, or
follow-up. **`// TODO:` comments** are unstructured prose hidden in code, with
no type, no severity, no author beyond `git blame`, and no way to thread a
discussion. **Issue trackers** are separate from the code they describe; they
collect bit-rot, can't address a specific span, and require context-switching
to a different application to learn anything about the file in front of you.

Each of these tools fails the same way: the observation is not a first-class,
addressable, durable artifact alongside the code. Qualifier's wager is that if
you make structured observations look like code — files in the repo, version
controlled, content-addressed, append-only, threadable — they stop evaporating.
A concern raised in February is still queryable in October. A reply written by
an agent threads to the human comment that prompted it. A resolution
supersedes the original signal without erasing it. Merges are clean under
normal workflows because the file format is designed for it. Tooling can read
every record because the envelope is uniform.

The same skeleton that holds a human concern also holds a license declaration,
a security advisory, or a performance measurement. The format is a substrate;
annotations are simply its first and most-developed application. The cost of
adoption is one JSONL file per directory and a CLI; the payoff is that the
structured knowledge you produce — by hand, by review, by tool, by agent —
finally has somewhere to live where it accumulates instead of decays.

If you are forwarding this document to convince a teammate: the pitch is that
your team already produces this metadata. It is currently scattered across
five systems and lost on every merge. Qualifier gives it a single home, in
files, that survives.

## 1. Design Principles

1. **Files are the API.** The `.qual` format is the primary interface. Every
   tool — CLI, editor plugin, CI bot, coding agent — reads and writes the same
   files. No server, no database, no lock-in.

2. **VCS-native.** `.qual` files are append-only JSONL. They merge cleanly,
   diff readably, and blame usefully. Conflicts are structurally impossible
   under normal workflows (append-only + file-per-artifact).

3. **Open record types.** The format is a substrate, not a single application.
   The Metabox envelope is fixed; record bodies are typed and extensible. New
   record types extend the system without changing the envelope, and
   unrecognized types pass through harmlessly. Annotations are the primary
   record type and the reason qualifier exists, but the same skeleton supports
   license declarations, security advisories, performance measurements, build
   provenance, or any other structured observation about a software artifact.
   Choosing the format does not lock you into a single domain.

4. **Ambient annotation.** Record observations the moment you see them. No PR
   required, no review window, no formal ceremony. A human reading code can
   leave a `concern` in five seconds; an agent finishing a task can leave a
   `comment` to flag a follow-up; a scanner can drop a `security-advisory`
   into the same file. The practice is structurally enabled by append-only
   JSONL plus content-addressed records — adding a record never conflicts with
   another, and every record has a stable, addressable identity from the
   moment it is written.

5. **Deterministic record IDs.** A record's `id` is the BLAKE3 hash of its
   Metabox Canonical Form (§2.8). Identical inputs produce identical IDs
   on every implementation — no language-specific or library-specific drift.

6. **Propagation through the graph.** Quality is more than local. Software has
   dependencies. An artifact's *effective* quality is a function of its own
   annotations AND the effective quality of everything it depends on. A
   pristine binary that links a cursed library inherits the curse.

7. **Human-first, agent-friendly.** The CLI is designed for humans at a
   terminal. The JSONL format and library API are designed for agents and
   tooling. Both are first-class.

8. **Composable.** The record format uses the Metabox envelope — a uniform
   frame (who said something about which subject) wrapping typed payloads
   (what they said). Records compose into threads via `references`, into
   chains via `supersedes`, and into graphs via `dependency` records.

9. **Interoperable.** Qualifier records project losslessly into in-toto
   annotation predicates. SARIF results import into qualifier annotations.
   The format bridges the gap between supply-chain annotation frameworks and
   human-scale quality tracking.

## 2. Record Model

### 2.1 Records

A **record** is a single, immutable, content-addressed JSON object that says
something about a software artifact. Records are the atoms of the system.

Every record has a **Metabox envelope** — a fixed set of fields that identify
*who* said *what kind of thing* about *which subject* and *when* — plus a
**body** object containing type-specific fields.

### 2.2 Metabox Envelope

Every record uses the [Metabox](METABOX.md) envelope format with these fields:

| Field          | Type     | Required | Description |
|----------------|----------|----------|-------------|
| `metabox`      | string   | yes      | Envelope version. MUST be `"1"`. |
| `type`         | string   | yes*     | Record type identifier (see 2.5). *May be omitted in `.qual` files; defaults to `"annotation"`. |
| `subject`      | string   | yes      | Qualified name of the target artifact |
| `issuer`       | string   | yes      | Who or what created this record (URI) |
| `issuer_type`  | string   | no       | Issuer classification: `human`, `ai`, `tool`, `unknown` |
| `created_at`   | string   | yes      | RFC 3339 timestamp |
| `id`           | string   | yes      | Content-addressed BLAKE3 hash (see 2.8) |
| `body`         | object   | yes      | Type-specific payload — see §3 for body schemas by type |

These eight fields form the **uniform interface**. They are the same for every
record type, they are stable across spec revisions, and they are sufficient
to answer the questions "who said what kind of thing about what and when?"
without understanding the body.

### 2.3 Subject Names

A **subject** is any addressable unit of software that can be qualified.
Subjects are identified by a **qualified name** (a string), which SHOULD
correspond to a logical unit in the codebase:

- A file path: `src/parser.rs`
- A module: `crate::parser`
- A build target: `//services/auth:lib`
- A package: `pkg:npm/lodash@4.17.21`

Qualifier does not enforce a naming scheme. The names are opaque strings.
Conventions are a project-level decision.

#### 2.3.1 Subject Renames

Qualifier identifies subjects by their qualified name. Renaming a subject
(e.g., `src/parser.rs` to `src/ast_parser.rs`) requires the following steps:

1. Rename the `.qual` file to match the new subject name.
2. Update dependency records to reference the new name wherever the old name
   appeared (both as `subject` and in `depends_on` arrays).
3. **Note:** Existing records inside the renamed `.qual` file still contain the
   old `subject` field in their JSON. Since record IDs are content-addressed,
   changing the `subject` field would change the ID, breaking supersession
   chains.

The RECOMMENDED workflow after a rename is:

1. Rename the `.qual` file and update dependency records.
2. Run `qualifier compact <new-name> --snapshot` to collapse history into a
   fresh epoch under the new name.
3. Commit the rename and compacted file together.

### 2.4 Spans

A **span** identifies a sub-range within a subject. When present in the body,
the record addresses a specific region rather than the whole artifact.

```json
"span": {
  "start": { "line": 42 },
  "end": { "line": 58 }
}
```

A span is an object with these fields:

| Field          | Type   | Required | Description |
|----------------|--------|----------|-------------|
| `start`        | object | yes      | Start of the range (inclusive) |
| `end`          | object | no       | End of the range (inclusive). Defaults to `start`. |
| `content_hash` | string | no       | BLAKE3 hash of the spanned lines (see 2.4.3) |

Each position has:

| Field  | Type    | Required | Description |
|--------|---------|----------|-------------|
| `line` | integer | yes      | 1-indexed line number |
| `col`  | integer | no       | 1-indexed column number |

#### 2.4.1 Span Forms

```json
// Lines 42 through 58:
"span": {"start": {"line": 42}, "end": {"line": 58}}

// Line 42 only (end defaults to start):
"span": {"start": {"line": 42}}

// Columns 5–15 on line 42:
"span": {"start": {"line": 42, "col": 5}, "end": {"line": 42, "col": 15}}

// Cross-line range with column precision:
"span": {"start": {"line": 42, "col": 5}, "end": {"line": 58, "col": 80}}
```

#### 2.4.2 Span Normalization

Before hashing (see 2.8), spans are normalized:

- If `end` is absent, it is set equal to `start`.
- If `col` is absent from a position, it remains absent (not defaulted).
- `content_hash` is not modified during normalization. It passes through
  unchanged and participates in the record ID computation when present.

After normalization, `{"start":{"line":42}}` and
`{"start":{"line":42},"end":{"line":42}}` produce identical canonical forms
and therefore identical record IDs.

#### 2.4.3 Content Hashing

When `content_hash` is present, it records a BLAKE3 hash of the source lines
covered by the span at the time the annotation was created. This enables
**freshness checking** — detecting whether the annotated code has changed
since the annotation was written.

**Hash computation:**

1. Read the file identified by the record's `subject`.
2. Extract lines `start.line` through `end.line` (inclusive, 1-indexed).
   Columns are ignored — full lines are always hashed.
3. Join the extracted lines with `\n` (no trailing newline).
4. Compute the BLAKE3 hash of the resulting byte string.
5. Encode as lowercase hex.

**When computed:** The CLI auto-computes `content_hash` when creating span-
addressed annotations (via `flag`, `suggest`, `comment`, `approve`, `reject`,
`attest --span`, etc.) if the subject file exists and the span is within
bounds. If the file does not exist or the span extends beyond EOF, `content_hash`
is omitted.

**Relationship to `ref`:** The `ref` field pins an annotation to a VCS
revision (e.g., `git:3aba500`). `content_hash` pins the annotation to
specific file content. They are complementary: `ref` answers "which commit?"
while `content_hash` answers "has the code changed?"

**Freshness states:**

| State     | Meaning |
|-----------|---------|
| Fresh     | `content_hash` matches current file content |
| Drifted   | `content_hash` differs from current file content |
| Missing   | File not found or span beyond EOF |
| No hash   | Annotation has no `content_hash` (older or whole-file annotations) |

#### 2.4.4 Spans Address Subjects

Span-addressed records attach to their parent **subject**. An annotation
about `src/parser.rs` at span `{start: {line: 42}, end: {line: 58}}` is a
record about `src/parser.rs` that happens to point at lines 42–58.

Spans are addressing granularity. They tell you *where* within the subject
a signal applies but do not create separate addressing targets.

### 2.5 Record Types

The `type` field is a string that identifies the body schema. Implementations
MUST support the `annotation`, `epoch`, and `dependency` types. Additional
types defined in this spec are RECOMMENDED but not strictly required —
implementations that don't understand them MUST still preserve them (forward
compatibility).

| Type                 | Description |
|----------------------|-------------|
| `annotation`         | A quality signal (see 2.6) |
| `epoch`              | A compaction snapshot (see 3.2) |
| `dependency`         | A dependency edge (see 3.4) |
| `license`            | A license declaration (see 3.5) |
| `security-advisory`  | A known vulnerability or weakness (see 3.6) |
| `perf-measurement`   | A performance measurement (see 3.7) |

Implementations MUST ignore records with unrecognized types (forward
compatibility). Unrecognized records MUST be preserved during file operations
(compaction, rewriting) — they are opaque pass-through data.

When `type` is omitted in a `.qual` file, it defaults to `"annotation"`.
In canonical form (for hashing), `type` is always materialized.

### 2.6 Annotation Records

An **annotation** is a quality signal about a subject. It is the primary
record type and the reason qualifier exists.

Metabox envelope fields (section 2.2) plus body fields:

| Field           | Type     | Required | Description |
|-----------------|----------|----------|-------------|
| `detail`        | string   | no       | Extended description, markdown allowed |
| `kind`          | string   | yes      | The type of annotation (see 2.7) |
| `ref`           | string   | no       | VCS reference pin (e.g., `"git:3aba500"`). Opaque to qualifier. |
| `references`    | string   | no       | ID of a related record (see 2.11) |
| `span`          | object   | no       | Sub-artifact range (see 2.4) |
| `suggested_fix` | string   | no       | Actionable suggestion for improvement |
| `summary`       | string   | yes      | Human-readable one-liner |
| `supersedes`    | string   | no       | ID of a prior record this replaces (see 2.9) |
| `tags`          | string[] | no       | Freeform classification tags |

Body fields are listed in alphabetical order, which matches the Metabox
Canonical Form (MCF) serialization order.

**Example:**

```json
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:alice@example.com","issuer_type":"human","created_at":"2026-02-25T10:00:00Z","id":"a1b2c3d4...","body":{"kind":"concern","ref":"git:3aba500","span":{"start":{"line":42},"end":{"line":58}},"suggested_fix":"Use the ? operator instead of unwrap()","summary":"Panics on malformed input","tags":["robustness"]}}
```

**Shorthand (equivalent):** Since `type` defaults to `"annotation"`, it may
be omitted:

```json
{"metabox":"1","subject":"src/parser.rs","issuer":"mailto:alice@example.com","created_at":"2026-02-25T10:00:00Z","id":"a1b2c3d4...","body":{"kind":"concern","summary":"Panics on malformed input"}}
```

### 2.7 Annotation Kinds

The `kind` field is an open enum. The following kinds are defined by the spec;
implementations MUST support them and MAY define additional kinds.

| Kind          | Meaning |
|---------------|---------|
| `pass`        | The artifact meets a stated quality bar |
| `fail`        | The artifact does NOT meet a stated quality bar |
| `blocker`     | A blocking issue that must be resolved before release |
| `concern`     | A non-blocking issue worth tracking |
| `comment`     | An observation or discussion point with no scoring impact |
| `praise`      | Positive recognition of quality |
| `resolve`     | Closes a prior record via supersession |
| `suggestion`  | A proposed improvement (typically paired with `suggested_fix`) |
| `waiver`      | An acknowledged issue explicitly accepted (with rationale) |

#### 2.7.1 Sign Conventions

The kinds carry an implicit polarity that downstream tools (scoring,
filtering, gating) can use. Implementations layering numeric signals on
top SHOULD respect these signs:

| Kind          | Polarity |
|---------------|----------|
| `pass`        | positive |
| `praise`      | positive |
| `waiver`      | positive |
| `comment`     | neutral  |
| `resolve`     | neutral  |
| `concern`     | negative |
| `suggestion`  | negative |
| `fail`        | negative |
| `blocker`     | negative |

The format itself does not carry a numeric score. Tools MAY add custom
body fields (e.g., a `score` integer) and define their own evaluation
semantics on top of the kind polarity — see Appendix A for one possible
shape.

#### 2.7.2 Custom Kinds

Any string is a valid `kind`. Implementations SHOULD detect likely typos
(edit distance <= 2 from a built-in kind) and warn the user.

### 2.8 Record IDs & Canonical Form

A record ID is a lowercase hex-encoded BLAKE3 hash of the **Metabox
Canonical Form (MCF)** of the record, with the `id` field set to the empty
string `""` during hashing. This makes IDs deterministic and
content-addressed.

#### 2.8.1 Metabox Canonical Form (MCF)

To ensure that every implementation — regardless of language or JSON library —
produces identical bytes for the same record, the canonical serialization MUST
obey the following rules:

1. **Normalization.** Before serialization:
   - `type` MUST be materialized. If absent, set to `"annotation"`.
   - `metabox` MUST be materialized. If absent, set to `"1"`.
   - `span.end` MUST be materialized (in body). If absent, set equal to
     `span.start`.
   - `id` MUST be set to `""` (the empty string).

2. **Envelope field order.** Envelope fields MUST appear in this fixed order:
   `metabox`, `type`, `subject`, `issuer`, `issuer_type`, `created_at`, `id`,
   `body`. Optional envelope fields (`issuer_type`) are omitted when absent.

3. **Body field order.** Body fields MUST appear in lexicographic
   (alphabetical) order. Nested objects (like `span`) also have their fields
   in lexicographic order.

4. **Absent optional fields.** Optional fields whose value is absent (null,
   None, etc.) MUST be omitted entirely. `tags` MUST be omitted when the
   array is empty. The `id` field is the sole exception — it is always
   present (set to `""`).

5. **Whitespace.** No whitespace between tokens. No space after `:` or `,`.
   No trailing newline. The output is a single compact JSON line.

6. **No trailing commas.** Standard JSON — no trailing commas.

7. **String encoding.** Standard JSON escaping (RFC 8259 Section 7).
   Implementations MUST NOT add escapes beyond what JSON requires.

8. **Number encoding.** Integers serialize as bare decimal with no leading
   zeros, no decimal point, no exponent. Negative values use a leading `-`.

See the [Metabox specification](METABOX.md) for the full MCF definition.

#### 2.8.2 Example

Given an annotation with no optional body fields, the MCF is:

```json
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:alice@example.com","created_at":"2026-02-24T10:00:00Z","id":"","body":{"kind":"concern","summary":"Panics on malformed input"}}
```

With a span and issuer_type:

```json
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:alice@example.com","issuer_type":"human","created_at":"2026-02-24T10:00:00Z","id":"","body":{"kind":"concern","span":{"start":{"line":42},"end":{"line":42}},"summary":"Panics on malformed input"}}
```

Note that `span.end` has been materialized (it was omitted in the input,
defaulting to `start`), body fields appear in alphabetical order, and
`issuer_type` is in the envelope between `issuer` and `created_at`.

> **Rationale.** MCF extends the behavior of serde_json with
> `#[serde(skip_serializing_if)]` annotations. Alphabetical body field
> ordering is simpler than per-type field orders and eliminates the need for
> type-specific canonical form definitions.

### 2.9 Supersession

Records are immutable once written. To "update" a signal, you write a new
annotation with a `supersedes` field (in the body) pointing to the prior
record's `id`.

**Constraints:**

- The superseding and superseded records MUST have the same `subject` field.
  Cross-subject supersession is forbidden. Implementations MUST reject it.
- The `span` field MAY differ between superseder and superseded. (The
  problematic code may have moved.)
- Supersession chains MUST be acyclic. Implementations MUST detect and reject
  cycles.
- When evaluating active records, a superseded record MUST be excluded.
  Only the tip of each chain is active.
- Dangling `supersedes` references (pointing to IDs not present in the current
  file set) are allowed. The referencing record remains active.

**Resolve pattern:** A `resolve`-kind annotation supersedes its target,
withdrawing the target from the active set. This is the canonical way to
close an issue — the superseded record is no longer surfaced and the
resolve record stands as the visible tombstone.

### 2.10 The `.qual` File Format

A `.qual` file is a UTF-8 encoded file where each line is a complete JSON
object representing one record. This is JSONL (JSON Lines).

**Placement:** A `.qual` file can contain records for any subjects in its
directory or subdirectories. The `subject` field in each record is the
authoritative identifier — not the filename.

**Layout strategies:**

| Strategy | Example | Pros | Cons |
|----------|---------|------|------|
| **Per-directory** (recommended) | `src/.qual` | Clean tree, good merge behavior | Slightly more merge contention than 1:1 |
| Per-file | `src/parser.rs.qual` | Maximum merge isolation | Noisy file tree |
| Per-project | `.qual` at repo root | Simplest setup | High merge contention |

All layouts are backwards-compatible and can coexist in the same project.

**Rules:**
- Each line MUST be a valid JSON object conforming to a known or unknown
  record type.
- Lines MUST be separated by a single `\n` (LF).
- The file MUST end with a trailing `\n`.
- Empty lines and lines starting with `//` are ignored (comments).
- Implementations MUST preserve ordering; older records come first.
- New records MUST be appended, never inserted.
- The sole exception to append-only is **compaction** (see 3.3), which
  rewrites the file.

**Example (mixed record types):**

```jsonl
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:alice@example.com","issuer_type":"human","created_at":"2026-02-24T10:00:00Z","id":"a1b2c3d4...","body":{"kind":"concern","ref":"git:3aba500","span":{"start":{"line":42},"end":{"line":58}},"suggested_fix":"Replace .unwrap() with proper error propagation","summary":"Panics on malformed UTF-8 input","tags":["robustness","error-handling"]}}
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:bob@example.com","issuer_type":"human","created_at":"2026-02-24T11:00:00Z","id":"e5f6a7b8...","body":{"kind":"praise","summary":"Excellent property-based test coverage","tags":["testing"]}}
```

### 2.11 References

The `references` body field provides a lightweight "re:" pointer from one
annotation to another. Unlike `supersedes` (which removes the referenced
record from the active set), `references` is purely informational — both
the original and the referencing record remain active.

**Semantics:**

- A `references` value is a single record ID string.
- The referenced record is NOT filtered out. Both records remain active.
- Cross-subject references are allowed. An annotation on `src/lexer.rs`
  MAY reference a record on `src/parser.rs` ("see also").
- Dangling references are allowed (same policy as `supersedes`). The
  referenced record may live in a different file or not be loaded.
- Self-references are forbidden. An annotation MUST NOT reference its own
  ID. Implementations MUST reject this at write time.

**Use cases:**

- Reply threads: an AI follow-up to a human observation.
- Resolution chains: "this addresses the concern raised in <id>".
- Cross-file commentary: "see also the related concern on lexer.rs".

**Threading semantics:** Records referencing the same parent form a thread.
Implementations SHOULD display these as threaded conversations with
tree-drawing characters (`├──`, `└──`). Reply depth is unbounded — a reply
to a reply is a valid thread.

**Example:**

```json
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:bob@example.com","created_at":"2026-03-01T10:00:00Z","id":"b2c3d4e5...","body":{"kind":"comment","references":"a1b2c3d4...","summary":"This was addressed in the latest refactor"}}
```

**Full lifecycle example (flag → reply → resolve):**

```jsonl
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:alice@example.com","created_at":"2026-03-01T09:00:00Z","id":"a1b2c3d4...","body":{"kind":"concern","span":{"start":{"line":42}},"summary":"Panics on malformed input"}}
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:bob@example.com","created_at":"2026-03-01T10:00:00Z","id":"b2c3d4e5...","body":{"kind":"comment","references":"a1b2c3d4...","summary":"Good catch — fixed in latest commit"}}
{"metabox":"1","type":"annotation","subject":"src/parser.rs","issuer":"mailto:alice@example.com","created_at":"2026-03-01T11:00:00Z","id":"c3d4e5f6...","body":{"kind":"resolve","summary":"Resolved","supersedes":"a1b2c3d4..."}}
```

After the resolve, the original concern's `-10` is withdrawn from scoring.
The reply remains visible in the thread for context.

## 3. Record Type Specifications

### 3.1 Annotation (`type: "annotation"`)

Defined in section 2.6. This is the primary record type.

### 3.2 Epoch (`type: "epoch"`)

An **epoch** is a synthetic compaction summary produced by the compactor. It
replaces a set of records with a single record that preserves their refs.

Body fields (alphabetical):

| Field         | Type     | Required | Description |
|---------------|----------|----------|-------------|
| `refs`        | string[] | yes      | IDs of the compacted records |
| `span`        | object   | no       | Sub-artifact range |
| `summary`     | string   | yes      | `"Compacted from N records"` |

Epoch records MUST set `issuer` to `"urn:qualifier:compact"` and
`issuer_type` to `"tool"` (in the envelope).

**Example:**

```json
{"metabox":"1","type":"epoch","subject":"src/parser.rs","issuer":"urn:qualifier:compact","issuer_type":"tool","created_at":"2026-02-25T12:00:00Z","id":"f9e8d7c6...","body":{"refs":["a1b2...","c3d4..."],"summary":"Compacted from 12 records"}}
```

The `refs` field exists for auditability — it lets you trace back (via VCS
history) to the individual records that were folded in.

### 3.3 Compaction

Append-only files grow without bound. **Compaction** is the mechanism for
reclaiming space.

A compaction rewrites a `.qual` file by:

1. **Pruning** all superseded records. If record B supersedes A, only B is
   retained. The entire chain collapses to its tip.
2. **Optionally snapshotting.** When `--snapshot` is passed, all surviving
   records for each subject are replaced by a single epoch record.

#### 3.3.1 Compaction Rules

- Compaction MUST be explicit and user-initiated — never automatic or silent.
- Compaction MUST preserve records of unrecognized types (they are opaque
  pass-through).
- After compaction, the file is a valid `.qual` file. No special reader
  support is needed.
- `qualifier compact --dry-run` MUST be supported.

### 3.4 Dependency (`type: "dependency"`)

A **dependency** record declares directed dependency edges from one subject
to others.

Body fields:

| Field        | Type     | Required | Description |
|--------------|----------|----------|-------------|
| `depends_on` | string[] | yes      | Subject names this subject depends on |

**Example:**

```json
{"metabox":"1","type":"dependency","subject":"bin/server","issuer":"https://build.example.com","created_at":"2026-02-25T10:00:00Z","id":"1a2b3c4d...","body":{"depends_on":["lib/auth","lib/http","lib/db"]}}
```

The dependency graph implied by these records MUST be a DAG.

Dependency records are wire-format-level: they declare edges that
downstream tools can use to propagate signals across artifacts. The
reference CLI does not consume them today (the `qualifier graph` command
and built-in graph engine were yanked along with scoring), but they
round-trip through `.qual` files unchanged.

### 3.5 License (`type: "license"`)

A **license** record declares the licensing terms that apply to a subject.
License records are typically produced by a license scanner or written by
hand during a licensing audit.

Body fields:

| Field        | Type    | Required | Description |
|--------------|---------|----------|-------------|
| `confidence` | number  | no       | Detector confidence in `[0.0, 1.0]`. Omit for hand-asserted records. |
| `evidence`   | string  | no       | Free-form provenance for the assertion (e.g., `"LICENSE file SHA256:abc..."`, `"package.json#license"`). |
| `spdx_id`    | string  | yes      | SPDX license identifier (e.g., `"MIT"`, `"Apache-2.0"`, `"GPL-3.0-or-later"`). |

**Example:**

```json
{"metabox":"1","type":"license","subject":"vendor/lodash","issuer":"https://license-scanner.example.com","issuer_type":"tool","created_at":"2026-03-01T10:00:00Z","id":"...","body":{"confidence":0.98,"evidence":"LICENSE file SHA256:9f86d081...","spdx_id":"MIT"}}
```

A license record documents an attribute of the subject; if a licensing
problem warrants a quality signal, write a separate `annotation` (e.g.,
`kind: "blocker"`) and optionally `references` the license record.

### 3.6 Security Advisory (`type: "security-advisory"`)

A **security-advisory** record records a known vulnerability or weakness
affecting a subject. Records of this type are typically produced by a
vulnerability scanner, an SBOM tool, or written by hand when triaging a CVE.

Body fields:

| Field               | Type   | Required | Description |
|---------------------|--------|----------|-------------|
| `affected_versions` | string | no       | Version range expression (e.g., `"<1.4.2"`, `">=2.0.0,<2.3.1"`). |
| `cve_id`            | string | no       | CVE identifier (e.g., `"CVE-2024-1234"`). |
| `cwe_id`            | string | no       | CWE identifier (e.g., `"CWE-79"`). |
| `severity`          | string | yes      | One of `critical`, `high`, `medium`, `low`, `info`. |
| `summary`           | string | yes      | Human-readable one-line description of the issue. |

At least one of `cve_id` or `cwe_id` SHOULD be present, but neither is
strictly required (some advisories predate CVE assignment or describe
project-specific issues).

**Example:**

```json
{"metabox":"1","type":"security-advisory","subject":"vendor/openssl","issuer":"https://osv.dev","issuer_type":"tool","created_at":"2026-03-01T10:00:00Z","id":"...","body":{"affected_versions":"<3.0.8","cve_id":"CVE-2023-0286","severity":"high","summary":"X.400 address type confusion in X.509 GeneralName"}}
```

To turn a security advisory into a quality signal, write an `annotation`
(e.g., `kind: "blocker"`) on the same subject that `references` the advisory.

### 3.7 Performance Measurement (`type: "perf-measurement"`)

A **perf-measurement** record captures a single performance measurement for
a subject. Records of this type are typically produced by a benchmark
harness, a profiler, or a CI job that records production telemetry.

Body fields:

| Field      | Type   | Required | Description |
|------------|--------|----------|-------------|
| `baseline` | number | no       | Reference value to compare against (e.g., the previous measurement). |
| `metric`   | string | yes      | Metric identifier (e.g., `"latency_p99_ms"`, `"throughput_rps"`, `"binary_size_bytes"`). |
| `unit`     | string | no       | Unit of measure (e.g., `"ms"`, `"req/s"`, `"bytes"`). May be embedded in the metric name; this field is for explicit cases. |
| `value`    | number | yes      | The measured value. |

**Example:**

```json
{"metabox":"1","type":"perf-measurement","subject":"bin/server","issuer":"https://ci.example.com","issuer_type":"tool","created_at":"2026-03-01T10:00:00Z","id":"...","body":{"baseline":42.0,"metric":"latency_p99_ms","unit":"ms","value":47.3}}
```

A regression worth flagging should be expressed as an `annotation` (e.g.,
`kind: "concern"` or `kind: "blocker"`) that may `references` the underlying
measurement record.

### 3.8 Defining New Record Types

Per design principle 3 (Open record types), implementations and integrations
MAY define new record types. New record types are identified by a string
value in the `type` field. Types defined outside this spec SHOULD use a URI
to avoid collisions:

```json
{"metabox":"1","type":"https://example.com/qualifier/build-provenance/v1","subject":"bin/server","issuer":"https://build.example.com","created_at":"...","id":"...","body":{"builder":"github-actions","commit":"abc123"}}
```

Types defined in this spec use short unqualified names (`annotation`,
`epoch`, `dependency`, `license`, `security-advisory`, `perf-measurement`).
The spec reserves all unqualified type names (strings that do not contain
`:` or `/`) for future standardization.

A record type specification MUST define the body fields, their types, and
which are required. Body fields are always serialized in lexicographic
order per MCF.

## 4. Layering Quality Signals on Top

The format itself does not prescribe a numeric model. Annotations carry a
`kind` (with implicit polarity, see §2.7.1) and a free-form body; tools
that want to compute aggregate quality signals layer on top by adding
custom body fields and defining their own evaluation semantics.

This section is an **example** of one such layer. Nothing here is required
of conforming implementations.

### 4.1 Example: A `score` body field

A tool MAY add a `score: integer` field to annotation bodies. Treat
`score` as a signed quality delta — negative for problems, positive for
positives, absent for neutral observations. A reasonable default mapping
follows the polarity table in §2.7.1:

| Kind          | Example default |
|---------------|-----------------|
| `pass`        | +20             |
| `fail`        | -20             |
| `blocker`     | -50             |
| `concern`     | -10             |
| `comment`     | absent          |
| `praise`      | +30             |
| `resolve`     | absent          |
| `suggestion`  | -5              |
| `waiver`      | +10             |

These are illustrative. A tool may pick any range or mapping that suits
its aggregation strategy.

### 4.2 Example: Aggregating across a subject

A tool that defines a `score` body field as above might define a **raw
score** for a subject as the sum of `score` fields of its active
(non-superseded) annotation records, clamped to a chosen range.

When a dependency graph (§3.4) is present, the tool might further define
an **effective score** that propagates negative signals along edges
(e.g., `effective(A) = min(raw(A), min(effective(D) for D in deps(A)))`),
so a problem in a leaf subject lowers the score of everything that
depends on it.

These are choices the tool makes, not invariants of the format. A
different tool might weight by `kind`, decay by age, or ignore signed
deltas entirely in favour of a categorical bar (e.g., "any active
`blocker` fails the build").

### 4.3 Span behaviour

Span-addressed records (§2.4) attach to their `subject`. Whatever
aggregation a tool defines, the span identifies where the signal applies
within the subject; the tool may surface span-level views for display,
but the canonical addressing unit is the subject.

## 5. Interoperability

### 5.1 in-toto Predicate Projection

Qualifier records project losslessly into [in-toto v1 Statement](https://github.com/in-toto/annotation/blob/main/spec/v1/statement.md)
predicates for use with DSSE signing and Sigstore distribution.

**Mapping (annotation):**

```json
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "src/parser.rs",
      "digest": {"blake3": "<artifact-content-hash>"}
    }
  ],
  "predicateType": "https://qualifier.dev/annotation/v1",
  "predicate": {
    "qualifier_id": "a1b2c3d4...",
    "kind": "concern",
    "span": {"start": {"line": 42}, "end": {"line": 58}},
    "summary": "Panics on malformed input",
    "tags": ["robustness"],
    "issuer": "mailto:alice@example.com",
    "issuer_type": "human",
    "created_at": "2026-02-25T10:00:00Z",
    "ref": "git:3aba500",
    "supersedes": null
  }
}
```

**Field mapping:**

| Qualifier field | in-toto location |
|----------------|------------------|
| `subject` | `subject[0].name` |
| `body.span` | `predicate.span` |
| `id` | `predicate.qualifier_id` |
| `issuer` | `predicate.issuer` (also DSSE signer) |
| `issuer_type` | `predicate.issuer_type` |
| All body fields | `predicate.*` |

The in-toto `subject[0].digest` contains the content hash of the artifact
file. This is populated by the signing tool, not by qualifier itself.
Qualifier's `id` is the hash of the *record*, not the *artifact*.

**Predicate type URIs:**

| Qualifier type | Predicate type URI |
|---------------|-------------------|
| `annotation` | `https://qualifier.dev/annotation/v1` |
| `epoch` | `https://qualifier.dev/epoch/v1` |
| `dependency` | `https://qualifier.dev/dependency/v1` |

### 5.2 SARIF Import

SARIF v2.1.0 results can be converted to qualifier annotations:

| SARIF field | Qualifier field |
|-------------|----------------|
| `result.locations[0].physicalLocation.artifactLocation.uri` | `subject` |
| `result.locations[0].physicalLocation.region.startLine` | `body.span.start.line` |
| `result.locations[0].physicalLocation.region.startColumn` | `body.span.start.col` |
| `result.locations[0].physicalLocation.region.endLine` | `body.span.end.line` |
| `result.locations[0].physicalLocation.region.endColumn` | `body.span.end.col` |
| `result.ruleId` | `body.kind` (as custom kind) |
| `result.level` | `body.kind` (`error` → `fail`, `warning` → `concern`, `note` → `comment`) |
| `result.message.text` | `body.summary` |
| `run.tool.driver.name` | `issuer` |
| (constant) | `issuer_type: "tool"` (envelope) |

## 6. CLI Interface

The CLI binary is named `qualifier`. Writes go through four verbs:
`record`, `reply`, `resolve`, and `emit`.

### 6.1 Core Commands

**Write commands:**

```
qualifier record <kind> <location> [message]    Record an annotation
qualifier reply <target> <message>              Reply to an existing record
qualifier resolve <target> [message]            Resolve (close) an existing record
qualifier emit <type> <subject> --body '<JSON>' Emit a raw record of any type
```

**Inspect commands:**

```
qualifier show <artifact>                 Show annotations for an artifact
qualifier ls [--kind <k>]                 List subjects by kind
qualifier praise <artifact>               Show who annotated an artifact and why
                                          (also available as the `blame` alias)
qualifier review [subject]                Check freshness of annotations
```

**Maintain commands:**

```
qualifier compact <artifact> [options]    Compact a .qual file (prune/snapshot)
```

### 6.2 `qualifier record`

The unified annotation-write verb. Replaces the old `attest`, `flag`,
`comment`, `suggest`, `approve`, and `reject` commands with a single
shape: `qualifier record <kind> <location> [message] [flags]`.

```
qualifier record concern src/parser.rs:42:58 "Panics on malformed input" \
  --suggested-fix "Use proper error propagation" \
  --tag robustness \
  --tag error-handling \
  --issuer "mailto:alice@example.com"
```

**Arguments:**

| Argument | Meaning |
|----------|---------|
| `<kind>` | One of `concern`, `comment`, `suggestion`, `pass`, `fail`, `blocker`, `praise`, `waiver`, `resolve`. Custom strings are allowed (per spec §2.7.2). |
| `<location>` | Subject path with optional span — see §6.2.1. |
| `[message]` | One-line summary. Becomes `body.summary`. Required in non-interactive mode unless `--stdin` is set. |

**Flags:** `--detail TEXT`, `--ref REF`, `--tag T1 --tag T2 ...`,
`--suggested-fix TEXT`, `--issuer URI`, `--issuer-type {human|ai|tool|unknown}`,
`--file PATH`, `--span SPEC` (overrides any span in `<location>`),
`--supersedes ID`, `--references ID`, `--stdin` (batch JSONL).

**Defaults:**

- When `--issuer` is omitted, defaults to the VCS user identity (see §8.4).
- When a span is given, `content_hash` is auto-computed if the source file
  is readable.

#### 6.2.1 Location and Span Syntax

The `<location>` argument folds the subject and an optional span into a
single string:

| Form | Meaning |
|------|---------|
| `src/parser.rs` | Whole file |
| `src/parser.rs:42` | Line 42 |
| `src/parser.rs:15:28` | Lines 15 through 28 |

The `--span` flag overrides any span parsed from `<location>` and accepts
the same forms plus column granularity:

| Form | Meaning | Equivalent `span` object |
|------|---------|--------------------------|
| `42` | Line 42 | `{"start":{"line":42},"end":{"line":42}}` |
| `42:58` | Lines 42 through 58 | `{"start":{"line":42},"end":{"line":58}}` |
| `42.5:58.80` | Line 42 col 5 through line 58 col 80 | `{"start":{"line":42,"col":5},"end":{"line":58,"col":80}}` |

#### 6.2.2 Batch Mode

`qualifier record --stdin` reads JSONL from stdin. Each line is one of:

- An overrides object: `{"kind":"...","location":"...","message":"...", ...}`
  with optional `detail`, `ref`, `tags`, `issuer`, `issuer_type`,
  `span`, `supersedes`, `references`, `suggested_fix`.
- A complete record (envelope + body), accepted for forward-compat.

### 6.3 `qualifier reply`

```
qualifier reply <target> <message>
```

Sugar over "kind=comment + references=`<target-id>`". The default kind is
`comment`; override with `--kind`.

`<target>` is either:

- An **id-prefix** (≥ 4 characters), or
- A **`<location>`** (e.g., `src/auth.rs:42`). A location resolves to the
  most-recent active record at that subject and span. If multiple active
  records share the most-recent timestamp, exit non-zero with a
  disambiguation list of `[id-prefix] kind L<line> "summary"`.

Same body flags as `qualifier record`.

### 6.4 `qualifier resolve`

```
qualifier resolve <target> [message]
```

Sugar over "kind=resolve + supersedes=`<target-id>`". `<target>` follows
the same id-prefix-or-location rules as `qualifier reply`. The default
summary is "Resolved" when `[message]` is omitted.

### 6.5 `qualifier emit`

```
qualifier emit <type> <subject> --body '<JSON>'
```

A raw, script-oriented write for novel or uncommon record types. The body
is passed through unchanged into the record's `body` field. For unknown
types the record round-trips via `Record::Unknown` (preserving the body
verbatim). For `--type annotation`, the body is validated against
`AnnotationBody`.

```
qualifier emit license src/lib.rs --body '{"spdx":"MIT"}' \
  --issuer "https://ci.example.com"

qualifier emit https://example.com/lint/v1 src/parser.rs \
  --body '{"rule":"no-panic","matches":3}'
```

`--stdin` reads JSONL where each line is a complete record. The
positional `<type>` and `<subject>`, when supplied, become defaults
applied to lines missing those fields.

#### 6.5.1 Example Workflow

```bash
# Record a concern at line 42
qualifier record concern src/parser.rs:42 "Panics on malformed input"

# See the concern
qualifier show src/parser.rs

# Reply to it (using ID prefix or location)
qualifier reply a1b2 "Good catch, fixed in latest commit"
qualifier reply src/parser.rs:42 "Good catch, fixed in latest commit"

# Close it
qualifier resolve a1b2

# The original concern is no longer surfaced
qualifier show src/parser.rs
```

### 6.6 `qualifier show`

```
qualifier show src/parser.rs

  src/parser.rs

  Records (4):
    concern  "Panics on malformed input"    alice  2026-02-24  a1b2c3d4
    ├── comment  "Good catch, fixed"        bob    2026-02-25  b2c3d4e5
    └── resolve  "Resolved"                 alice  2026-02-25  c3d4e5f6
    praise   "Excellent property test coverage"  bob  2026-02-24  e5f6a7b8
```

When annotations have spans, the line range is displayed. Use
`--line <n>` to filter to annotations overlapping a specific line.

`--all` shows all records including resolved/superseded ones (default hides
them). `--pretty` forces colored output when piped.

### 6.7 `qualifier ls`

```
qualifier ls --kind blocker
qualifier ls --unqualified
```

### 6.8 `qualifier compact`

```
qualifier compact src/parser.rs              # prune superseded records
qualifier compact src/parser.rs --snapshot   # collapse to a single epoch
qualifier compact src/parser.rs --dry-run    # preview without writing
qualifier compact --all                      # compact every .qual file
qualifier compact --all --dry-run            # preview repo-wide compaction
```

### 6.9 `qualifier review`

Check the freshness of span-addressed annotations against current file content.

```
qualifier review                          # check all annotations
qualifier review src/parser.rs            # check annotations for one subject
qualifier review --format json            # machine-readable output
qualifier review --no-ignore              # bypass ignore rules
```

**Human output:**

```
  FRESH    src/parser.rs:42    concern  "Panics on malformed input"
  DRIFTED  src/auth.rs:10:25   suggestion  "Consider using Result"
  MISSING  src/old.rs:1:20     blocker  "Memory leak"

3 annotations checked: 1 fresh, 1 drifted, 1 missing
```

Only active (non-superseded) annotations with spans that have a `content_hash`
are checked. Annotations without spans or without `content_hash` are skipped.

**JSON output** includes `status` (`fresh`, `drifted`, `missing`) and `detail`
with expected/actual hashes for drifted annotations or a reason for missing ones.

### 6.10 Configuration

Qualifier uses layered configuration. Precedence (highest wins):

| Priority | Source |
|----------|--------|
| 1 (highest) | CLI flags |
| 2 | Environment variables |
| 3 | Project config (`.qualifier.toml`) |
| 4 | User config (`~/.config/qualifier/config.toml`) |
| 5 (lowest) | Built-in defaults |

**Configuration keys:**

| Key         | CLI flag       | Env var              | Default |
|-------------|----------------|----------------------|---------|
| `issuer`    | `--issuer`     | `QUALIFIER_ISSUER`   | VCS identity (see 8.4) |
| `format`    | `--format`     | `QUALIFIER_FORMAT`   | `human` |

### 6.11 `qualifier praise`

Show who recorded annotations against an artifact and why. Available
under the alias `qualifier blame`; the canonical name is `praise` (the
tool tracks who helped, not who to blame). With `--vcs`, delegates to
the underlying VCS blame command for the subject's `.qual` file.

```
qualifier praise src/parser.rs
qualifier praise src/parser.rs --vcs
```

## 7. Library API

The `qualifier` crate exposes its library API from `src/lib.rs`. Library
consumers add `qualifier = { version = "0.4", default-features = false }` to
avoid pulling in CLI dependencies.

```rust
// qualifier::annotation — record types and core logic

/// A typed qualifier record. Dispatches on the `type` field in JSON.
pub enum Record {
    Annotation(Box<Annotation>),
    Epoch(Epoch),
    Dependency(DependencyRecord),
    Unknown(serde_json::Value),  // forward compatibility
}

impl Record {
    pub fn subject(&self) -> &str;
    pub fn id(&self) -> &str;
    pub fn supersedes(&self) -> Option<&str>;   // Annotation only
    pub fn references(&self) -> Option<&str>;   // Annotation only
    pub fn kind(&self) -> Option<&Kind>;        // Annotation only
    pub fn issuer_type(&self) -> Option<&IssuerType>;
    pub fn as_annotation(&self) -> Option<&Annotation>;
    pub fn as_epoch(&self) -> Option<&Epoch>;
}

pub struct Annotation {
    pub metabox: String,                    // always "1"
    pub record_type: String,                // "annotation"
    pub subject: String,
    pub issuer: String,
    pub issuer_type: Option<IssuerType>,
    pub created_at: DateTime<Utc>,
    pub id: String,
    pub body: AnnotationBody,
}

pub struct AnnotationBody {
    pub detail: Option<String>,
    pub kind: Kind,
    pub r#ref: Option<String>,
    pub references: Option<String>,
    pub span: Option<Span>,
    pub suggested_fix: Option<String>,
    pub summary: String,
    pub supersedes: Option<String>,
    pub tags: Vec<String>,
}

pub struct Epoch {
    pub metabox: String,                    // always "1"
    pub record_type: String,                // "epoch"
    pub subject: String,
    pub issuer: String,
    pub issuer_type: Option<IssuerType>,
    pub created_at: DateTime<Utc>,
    pub id: String,
    pub body: EpochBody,
}

pub struct EpochBody {
    pub refs: Vec<String>,
    pub span: Option<Span>,
    pub summary: String,
}

pub struct DependencyRecord {
    pub metabox: String,                    // always "1"
    pub record_type: String,                // "dependency"
    pub subject: String,
    pub issuer: String,
    pub issuer_type: Option<IssuerType>,
    pub created_at: DateTime<Utc>,
    pub id: String,
    pub body: DependencyBody,
}

pub struct DependencyBody {
    pub depends_on: Vec<String>,
}

pub struct Span {
    pub start: Position,
    pub end: Option<Position>,          // normalized to Some(start) before hashing
    pub content_hash: Option<String>,   // BLAKE3 of spanned lines
}

pub struct Position {
    pub line: u32,               // 1-indexed
    pub col: Option<u32>,        // 1-indexed, optional
}

pub enum Kind { Pass, Fail, Blocker, Concern, Comment, Resolve, Praise, Suggestion, Waiver, Custom(String) }
pub enum IssuerType { Human, Ai, Tool, Unknown }

pub fn generate_id(annotation: &Annotation) -> String;
pub fn generate_epoch_id(epoch: &Epoch) -> String;
pub fn generate_dependency_id(dep: &DependencyRecord) -> String;
pub fn generate_record_id(record: &Record) -> String;
pub fn validate(annotation: &Annotation) -> Vec<String>;
pub fn finalize(annotation: Annotation) -> Annotation;
pub fn finalize_epoch(epoch: Epoch) -> Epoch;
pub fn finalize_record(record: Record) -> Record;

// qualifier::qual_file
pub struct QualFile { pub path: PathBuf, pub subject: String, pub records: Vec<Record> }
pub fn parse(path: &Path) -> Result<QualFile>;
pub fn append(path: &Path, record: &Record) -> Result<()>;
pub fn discover(root: &Path, respect_ignore: bool) -> Result<Vec<QualFile>>;

// qualifier::content_hash — span freshness checking
pub fn compute_span_hash(file_path: &Path, span: &Span) -> Option<String>;
pub enum FreshnessStatus { Fresh, Drifted { expected, actual }, Missing { reason }, NoHash }
pub fn check_freshness(file_path: &Path, span: &Span) -> FreshnessStatus;

// qualifier::compact
pub struct CompactResult { pub before: usize, pub after: usize, pub pruned: usize }
pub fn filter_superseded(records: &[Record]) -> Vec<&Record>;
pub fn prune(qual_file: &QualFile) -> (QualFile, CompactResult);
pub fn snapshot(qual_file: &QualFile) -> (QualFile, CompactResult);
```

The library is the source of truth. The CLI is a thin wrapper around it.

## 8. VCS Integration

`.qual` files SHOULD be committed to version control. Qualifier is VCS-agnostic
— the append-only JSONL format is friendly to any system that tracks text files.

### 8.1 General Principles

- Append-only JSONL minimizes merge conflicts.
- Pre-compaction history is recoverable from VCS history.
- For collaborative repositories, configure your VCS to use union merges
  on `.qual` files so concurrent appends don't collide.

### 8.2 VCS-Specific Setup

| VCS        | Configuration |
|------------|---------------|
| Git        | Add `*.qual merge=union` to `.gitattributes` |
| Mercurial  | Add `**.qual = union` to `.hgrc` merge patterns |
| Other      | Configure equivalent union-merge behaviour for `*.qual` |

### 8.3 `qualifier blame`

Delegates to the underlying VCS blame/annotate command:

- Git: `git blame`
- Mercurial: `hg annotate`
- Fallback: not available (prints guidance)

### 8.4 Issuer Defaults

When `--issuer` is omitted:

- Git: `git config user.email`
- Mercurial: `hg config ui.username`
- Fallback: `mailto:$USER@localhost`

## 9. Agent Integration

Qualifier is designed to be used by AI coding agents. Key affordances:

- **Structured output:** `--format json` on `show` and `ls` commands.
- **Batch annotation:** `qualifier record --stdin` reads JSONL from stdin
  (overrides objects or full records). For non-annotation record types,
  `qualifier emit --stdin` accepts complete records.
- **Suggested fixes:** The `suggested_fix` body field gives agents a concrete
  action to take.
- **Span precision:** The `span` body field lets agents target specific line
  ranges, making annotations actionable without hunting for the relevant code.
- **Filtering by kind:** `qualifier ls --kind blocker --format json` gives
  agents a worklist of issues to address.
- **Continuous interaction:** `qualifier reply <id> <message>` lets agents
  respond to human signals with threaded follow-ups. `qualifier resolve <id>`
  lets agents close issues after fixes are applied.
- **Threading:** The `references` field enables agents to thread follow-up
  observations to prior signals, creating navigable conversation histories.

## 10. File Discovery

Qualifier discovers `.qual` files by walking the directory tree from the
project root. Each `.qual` file may contain records for multiple subjects
and multiple record types.

The project root is determined by searching upward for VCS markers (`.git`,
`.hg`, `.jj`, `.pijul`, `_FOSSIL_`, `.svn`).

### 10.1 Ignore Rules

By default, qualifier respects ignore rules from two sources during file
discovery:

1. **`.gitignore`** — Standard Git ignore files, including:
   - `.gitignore` files at any level of the tree
   - `.git/info/exclude` (per-repo excludes)
   - The global gitignore file (e.g., `~/.config/git/ignore`)
   - `.gitignore` files in parent directories above the project root
     (matching Git's own behavior in monorepos)

2. **`.qualignore`** — A qualifier-specific ignore file using the same
   syntax as `.gitignore`. Place a `.qualignore` file anywhere in the tree
   to exclude paths from qualifier's discovery walk. Useful for ignoring
   vendored code, generated files, or example directories that have `.qual`
   files you want qualifier to skip without affecting Git.

Paths matched by either source are excluded from all discovery commands:
`show`, `ls`, `compact`, `review`, and `praise`/`blame`.

### 10.2 `--no-ignore`

Pass `--no-ignore` to any discovery command to bypass all ignore rules.
This forces qualifier to walk every non-hidden directory and discover all
`.qual` files regardless of `.gitignore` or `.qualignore` entries.

### 10.3 Hidden Directories

Hidden directories (names starting with `.`) are always skipped during
discovery, regardless of ignore settings. This prevents qualifier from
descending into `.git`, `.vscode`, `.idea`, and similar tool directories.

Hidden *files* (like `.qual`) are not skipped — the per-directory `.qual`
layout depends on this.

## 11. Crate Structure

A single crate published as `qualifier` on crates.io.

```
qualifier/
├── Cargo.toml
├── SPEC.md                    # This document
├── METABOX.md                 # Metabox envelope specification
└── src/
    ├── lib.rs                 # Public library API
    ├── annotation.rs         # Record types, body structs, Kind, IssuerType, validation
    ├── content_hash.rs        # Span content hashing and freshness checking
    ├── qual_file.rs           # .qual file parsing, appending, discovery
    ├── compact.rs             # Compaction: prune and snapshot, supersession filtering
    ├── bin/
    │   └── qualifier.rs       # Binary entry point
    └── cli/                   # CLI module (behind "cli" feature)
        ├── mod.rs
        ├── config.rs
        ├── output.rs
        ├── span_context.rs
        └── commands/
            ├── mod.rs
            ├── record.rs         # qualifier record (unified annotation write)
            ├── reply.rs          # qualifier reply (id-prefix or location)
            ├── resolve.rs        # qualifier resolve (id-prefix or location)
            ├── emit.rs           # qualifier emit (raw record write)
            ├── freshness.rs      # qualifier review (freshness checking)
            ├── show.rs
            ├── ls.rs
            ├── compact.rs
            ├── praise.rs         # qualifier praise (alias: blame)
            └── haiku.rs
```

```toml
[features]
default = ["cli"]
cli = ["dep:clap", "dep:comfy-table", "dep:figment"]
```

## 12. Future Considerations (Out of Scope)

These are explicitly **not** part of v0.3 but are anticipated:

- **First-class scoring layer:** A built-in implementation of the example
  scoring model in §4 (`qualifier score`, `qualifier check`, dependency
  propagation), gated behind a feature flag.
- **Dependency graph engine:** A built-in graph (`qualifier graph` for
  visualization, plus traversal helpers used by the scoring layer above).
  Dependency *records* (§3.4) remain in the wire format today; the engine
  was yanked alongside scoring.
- **Project bootstrap (`qualifier init`):** Convenience scaffolding for
  per-project setup (VCS merge config, ignore file). Works without it
  today; reintroduced when there's a clear win.
- **Policy records** (`type: "policy"`): Project-level rules, required kinds,
  and gate criteria — expressed as records in the same stream.
- **Editor plugins:** LSP-based inline display of annotations, with
  span-aware gutter annotations.
- **DSSE signing:** `qualifier sign` to wrap records in DSSE envelopes for
  supply-chain distribution via Sigstore.
- **`qualifier import-sarif`:** First-class SARIF import command.
- **`qualifier rename`:** Automated subject rename with `.qual` file and
  dependency migration.
- **`qualifier watch`:** File-watcher mode for continuous scoring.
- **Remote aggregation:** Qualifier servers for cross-repository views.

---

*The Koalafier has spoken. Now go qualify some code.*
