qualifier 0.3.0

Deterministic quality attestations for software artifacts
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
# Qualifier Specification

**Version:** 0.3.0-draft
**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 — and to compute aggregate quality scores that
propagate through dependency graphs.

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.

## 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. **Deterministic scoring.** Given identical `.qual` files and an identical
   dependency graph, every implementation MUST produce identical quality scores.
   No floating point, no random weights — just deterministic integer arithmetic.

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

5. **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.

6. **Composable.** The record format uses the Metabox envelope — a uniform
   frame (who said something about which subject) wrapping typed payloads
   (what they said). New record types extend the system without changing the
   envelope. Unknown types pass through harmlessly.

7. **Interoperable.** Qualifier records project losslessly into in-toto
   attestation predicates. SARIF results import into qualifier attestations.
   The format bridges the gap between supply-chain attestation 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 `"attestation"`. |
| `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 2.6, 3.2, 3.4) |

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 two position fields:

| Field   | Type   | Required | Description |
|---------|--------|----------|-------------|
| `start` | object | yes      | Start of the range (inclusive) |
| `end`   | object | no       | End of the range (inclusive). Defaults to `start`. |

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).

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

#### 2.4.3 Span Scoring

Span-addressed records contribute to the score of their parent **subject**.
An attestation about `src/parser.rs` at span `{start: {line: 42}, end: {line: 58}}`
contributes to the raw score of `src/parser.rs`, not to a separate span-level
score.

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

> **Rationale.** Span-level scoring would be extremely noisy for most
> workflows. Subject-level aggregation is the right default. Future
> extensions MAY introduce opt-in span-level scoring.

### 2.5 Record Types

The `type` field is a string that identifies the body schema. Implementations
MUST support the following types:

| Type            | Description |
|-----------------|-------------|
| `attestation`   | A quality signal (see 2.6) |
| `epoch`         | A compaction snapshot (see 3.2) |
| `dependency`    | A dependency edge (see 3.4) |

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 `"attestation"`.
In canonical form (for hashing), `type` is always materialized.

### 2.6 Attestation Records

An **attestation** 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 attestation (see 2.7) |
| `ref`           | string   | no       | VCS reference pin (e.g., `"git:3aba500"`). Opaque to qualifier. |
| `score`         | integer  | yes      | Signed quality delta, -100..100 |
| `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":"attestation","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","score":-10,"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 `"attestation"`, 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","score":-10,"summary":"Panics on malformed input"}}
```

### 2.7 Attestation 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 |
| `praise`      | Positive recognition of quality |
| `suggestion`  | A proposed improvement (typically paired with `suggested_fix`) |
| `waiver`      | An acknowledged issue explicitly accepted (with rationale) |

#### 2.7.1 Recommended Score Ranges

The following table provides RECOMMENDED default scores for each kind.
Implementations and users MAY deviate, but SHOULD maintain sign consistency:
positive kinds SHOULD have positive scores, and negative kinds SHOULD have
negative scores.

| Kind          | Default Score | Recommended Range | Sign |
|---------------|---------------|-------------------|------|
| `pass`        | +20           | +10 to +50        | positive |
| `fail`        | -20           | -10 to -50        | negative |
| `blocker`     | -50           | -30 to -100       | negative |
| `concern`     | -10           | -5 to -30         | negative |
| `praise`      | +30           | +10 to +50        | positive |
| `suggestion`  | -5            | -5 to -15         | negative |
| `waiver`      | +10           | 0 to +30          | positive |

When `--score` is omitted from `qualifier attest`, the CLI SHOULD use the
default score for the given kind. `--score` always takes precedence.

These are guidance, not constraints. Implementations MUST NOT reject an
attestation solely because its score falls outside the recommended range.

#### 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 `"attestation"`.
   - `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 attestation with no optional body fields, the MCF is:

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

With a span and issuer_type:

```json
{"metabox":"1","type":"attestation","subject":"src/parser.rs","issuer":"mailto:alice@example.com","issuer_type":"human","created_at":"2026-02-24T10:00:00Z","id":"","body":{"kind":"concern","score":-30,"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
attestation 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 computing scores, a superseded record MUST be excluded. Only the tip
  of each chain contributes.
- Dangling `supersedes` references (pointing to IDs not present in the current
  file set) are allowed. The referencing record remains active.

### 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":"attestation","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","score":-30,"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":"attestation","subject":"src/parser.rs","issuer":"mailto:bob@example.com","issuer_type":"human","created_at":"2026-02-24T11:00:00Z","id":"e5f6a7b8...","body":{"kind":"praise","score":40,"summary":"Excellent property-based test coverage","tags":["testing"]}}
```

## 3. Record Type Specifications

### 3.1 Attestation (`type: "attestation"`)

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 attestations with a single record that preserves the net
score.

Body fields (alphabetical):

| Field         | Type     | Required | Description |
|---------------|----------|----------|-------------|
| `refs`        | string[] | yes      | IDs of the compacted records |
| `score`       | integer  | yes      | Raw score at compaction time |
| `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..."],"score":10,"summary":"Compacted from 12 records"}}
```

Epoch records are treated as normal scored records by the scoring engine. The
`refs` field exists solely 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 while preserving scoring correctness.

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 NOT change the raw score of any subject. This is the
  invariant. If compaction changes a score, the implementation has a bug.
- 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 MUST be a DAG. Implementations MUST detect and reject
cycles.

#### 3.4.1 Dependency Graph Sources

Qualifier accepts dependency information from two sources:

1. **Dependency records in `.qual` files** — as defined above.
2. **Legacy graph file** — a standalone JSONL file (conventionally
   `qualifier.graph.jsonl`) with simplified dependency declarations:

```jsonl
{"subject":"bin/server","depends_on":["lib/auth","lib/http","lib/db"]}
{"subject":"lib/auth","depends_on":["lib/crypto"]}
```

Both sources are merged when computing effective scores. When both declare
edges for the same subject, the union of all `depends_on` arrays is used.

### 3.5 Defining 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/license/v1","subject":"src/parser.rs","issuer":"https://license-scanner.example.com","created_at":"...","id":"...","body":{"license":"MIT"}}
```

Types defined in this spec use short aliases (`attestation`, `epoch`,
`dependency`). The spec reserves all unqualified type names (strings that
do not contain `:` or `/`) for future standardization.

A record type specification MUST define:

1. The body fields, their types, and which are required.
2. How the type interacts with scoring (if at all).

Body fields are always serialized in lexicographic order per MCF.

## 4. Scoring

### 4.1 Raw Score

The **raw score** of a subject is the sum of the `score` fields of all
non-superseded attestation and epoch records for that subject, clamped to
`[-100, 100]`.

```
raw_score(A) = clamp(-100, 100, sum(record.body.score for active scored records of A))
```

A subject with no scored records has a raw score of **0** (unqualified).

Only records of types that carry a `score` field (`attestation`, `epoch`)
contribute to scoring. Dependency records and unknown types do not.

### 4.2 Effective Score

The **effective score** of a subject is a function of its raw score and the
effective scores of its dependencies:

```
effective_score(A) = min(raw_score(A), min(effective_score(D) for D in deps(A)))
```

Your effective score can never be higher than your worst dependency's effective
score. Quality flows downhill.

If a subject has no dependencies, its effective score equals its raw score.

If a subject has no scored records but has dependencies, its effective score
is the minimum effective score of its dependencies (the "inherited floor").

### 4.3 Span Scoring

Span-addressed records contribute to the raw score of their `subject`. The
span is informational — it identifies where a signal applies within the
subject, but scoring aggregates at the subject level.

This means `qualifier score src/parser.rs` reports one score for the file,
even if individual attestations target different line ranges.

Implementations MAY offer span-level filtering for display (e.g.,
`qualifier show src/parser.rs --line 42` shows only attestations whose spans
overlap line 42), but this is a presentation concern, not a scoring concern.

### 4.4 Score Status

Implementations SHOULD report a human-readable status for each subject:

| Condition | Status |
|-----------|--------|
| effective < 0 | `blocker` |
| effective = 0, limited by dependency | `unqualified (limited)` |
| effective = 0 | `unqualified` |
| effective >= 60, limited by dependency | `healthy (limited)` |
| effective >= 60 | `healthy` |
| limited by dependency | `ok (limited)` |
| otherwise | `ok` |

A subject is "limited" when its effective score is lower than its raw score
due to a dependency constraint.

## 5. Interoperability

### 5.1 in-toto Predicate Projection

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

**Mapping (attestation):**

```json
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "src/parser.rs",
      "digest": {"blake3": "<artifact-content-hash>"}
    }
  ],
  "predicateType": "https://qualifier.dev/attestation/v1",
  "predicate": {
    "qualifier_id": "a1b2c3d4...",
    "kind": "concern",
    "score": -10,
    "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 |
|---------------|-------------------|
| `attestation` | `https://qualifier.dev/attestation/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 attestations:

| 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.score` (see mapping below) |
| `result.message.text` | `body.summary` |
| `run.tool.driver.name` | `issuer` |
| (constant) | `issuer_type: "tool"` (envelope) |

**Level-to-score mapping:**

| SARIF level | Default score |
|-------------|---------------|
| `error` | -20 |
| `warning` | -10 |
| `note` | -5 |
| `none` | 0 |

Implementations providing SARIF import SHOULD allow users to override these
defaults.

## 6. CLI Interface

The CLI binary is named `qualifier`.

### 6.1 Core Commands

```
qualifier attest <artifact> [options]     Add an attestation
qualifier show <artifact>                 Show attestations and scores
qualifier score [artifact...]             Compute and display scores
qualifier ls [--below <n>] [--kind <k>]   List subjects by score/kind
qualifier graph [--format dot|json]        Visualize the dependency graph
qualifier check [--min-score <n>]          CI gate: exit non-zero if below threshold
qualifier compact <artifact> [options]     Compact a .qual file (prune/snapshot)
qualifier init                             Initialize qualifier in a repo
qualifier blame <artifact>                 Per-line VCS attribution for a .qual file
```

### 6.2 `qualifier attest`

Interactive and non-interactive attestation creation.

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

#### 6.2.1 Span Syntax

The `--span` flag accepts the following forms:

| 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}}` |

When `--span` is omitted, no span is set (the attestation addresses the whole
subject).

#### 6.2.2 Other Flags

`--summary` is required in non-interactive mode.

When `--score` is omitted, the CLI uses the recommended default score for the
given kind (see section 2.7.1).

`--file <path>` writes the attestation to a specific `.qual` file instead
of using the default layout resolution.

When `--issuer` is omitted, defaults to the VCS user identity (see 8.4).

### 6.3 `qualifier show`

```
qualifier show src/parser.rs

  src/parser.rs
  Raw score:       10
  Effective score: -20 (limited by lib/crypto)

  Records (2):
    [-30] concern  L42–58 "Panics on malformed input"    alice  2026-02-24
    [+40] praise          "Excellent property test coverage"  bob  2026-02-24
```

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

### 6.4 `qualifier score`

```
qualifier score

  SUBJECT               RAW    EFF   STATUS
  lib/crypto            -20    -20   ██░░░░░░░░  blocker
  lib/auth               60    -20   ██░░░░░░░░  blocker
  lib/http               80     80   ████████░░  healthy
  bin/server             45    -20   ██░░░░░░░░  blocker
```

### 6.5 `qualifier check`

Returns exit code 0 if all subjects meet the threshold, non-zero otherwise.

```
qualifier check --min-score 0
```

### 6.6 `qualifier ls`

```
qualifier ls --below 0
qualifier ls --kind blocker
qualifier ls --unqualified
```

### 6.7 `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.8 `qualifier init`

```
qualifier init
  Created qualifier.graph.jsonl
  Detected VCS: git
  Added *.qual merge=union to .gitattributes
```

### 6.9 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 |
|-------------|----------------|----------------------|---------|
| `graph`     | `--graph`      | `QUALIFIER_GRAPH`    | `qualifier.graph.jsonl` |
| `issuer`    | `--issuer`     | `QUALIFIER_ISSUER`   | VCS identity (see 8.4) |
| `format`    | `--format`     | `QUALIFIER_FORMAT`   | `human` |
| `min_score` | `--min-score`  | `QUALIFIER_MIN_SCORE`| `0` |

### 6.10 `qualifier blame`

Delegates to the underlying VCS blame command for the subject's `.qual` file.

```
qualifier blame src/parser.rs
```

## 7. Library API

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

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

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

impl Record {
    pub fn subject(&self) -> &str;
    pub fn id(&self) -> &str;
    pub fn score(&self) -> Option<i32>;         // Attestation | Epoch
    pub fn supersedes(&self) -> Option<&str>;   // Attestation only
    pub fn kind(&self) -> Option<&Kind>;        // Attestation only
    pub fn issuer_type(&self) -> Option<&IssuerType>;
    pub fn as_attestation(&self) -> Option<&Attestation>;
    pub fn as_epoch(&self) -> Option<&Epoch>;
    pub fn is_scored(&self) -> bool;            // Attestation | Epoch
}

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

pub struct AttestationBody {
    pub detail: Option<String>,
    pub kind: Kind,
    pub r#ref: Option<String>,
    pub score: i32,
    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 score: i32,
    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 struct Position {
    pub line: u32,               // 1-indexed
    pub col: Option<u32>,        // 1-indexed, optional
}

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

pub fn generate_id(attestation: &Attestation) -> 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(attestation: &Attestation) -> Vec<String>;
pub fn finalize(attestation: Attestation) -> Attestation;
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::scoring
pub struct ScoreReport { pub raw: i32, pub effective: i32, pub limiting_path: Option<Vec<String>> }
pub fn raw_score(records: &[Record]) -> i32;
pub fn effective_scores(graph: &DependencyGraph, qual_files: &[QualFile]) -> HashMap<String, ScoreReport>;

// qualifier::compact
pub struct CompactResult { pub before: usize, pub after: usize, pub pruned: usize }
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.
- `qualifier init` detects the active VCS and applies appropriate configuration.

### 8.2 VCS-Specific Setup

| VCS        | Action |
|------------|--------|
| Git        | Adds `*.qual merge=union` to `.gitattributes` |
| Mercurial  | Adds `**.qual = union` merge pattern to `.hgrc` |
| Other      | Prints guidance for manual merge configuration |

### 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 `score`, `show`, and `ls` commands.
- **Batch attestation:** `qualifier attest --stdin` reads JSONL from stdin.
- **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 attestations actionable without hunting for the relevant code.
- **Priority ordering:** `qualifier ls --below 0 --format json` gives agents a
  prioritized worklist.

## 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`) or a `qualifier.graph.jsonl`
file, whichever is found first.

### 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:
`score`, `show`, `check`, `ls`, `compact`, 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
├── qualifier.graph.jsonl      # Example / self-hosted graph
└── src/
    ├── lib.rs                 # Public library API
    ├── attestation.rs         # Record types, body structs, Kind, IssuerType, validation
    ├── qual_file.rs           # .qual file parsing, appending, discovery
    ├── graph.rs               # Dependency graph loading, cycle detection
    ├── scoring.rs             # Raw + effective score computation
    ├── compact.rs             # Compaction: prune and snapshot
    ├── bin/
    │   └── qualifier.rs       # Binary entry point
    └── cli/                   # CLI module (behind "cli" feature)
        ├── mod.rs
        ├── config.rs
        ├── output.rs
        └── commands/
            ├── mod.rs
            ├── attest.rs
            ├── show.rs
            ├── score.rs
            ├── ls.rs
            ├── check.rs
            ├── compact.rs
            ├── graph_cmd.rs
            ├── init.rs
            └── blame.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:

- **Policy records** (`type: "policy"`): Project-level scoring rules, required
  kinds, and gate criteria — expressed as records in the same stream.
- **Span-level scoring:** Opt-in scoring at sub-artifact granularity.
- **Editor plugins:** LSP-based inline display of scores and attestations,
  with span-aware gutter annotations.
- **DSSE signing:** `qualifier sign` to wrap records in DSSE envelopes for
  supply-chain distribution via Sigstore.
- **Decay:** Time-based score decay to encourage re-qualification.
- **`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.*