qualifier 0.6.1

Deterministic quality annotations 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
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
# 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.*