Qualifier Specification
v0.4.0 — the complete record format and CLI specification.
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 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
-
Files are the API. The
.qualformat 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. -
VCS-native.
.qualfiles are append-only JSONL. They merge cleanly, diff readably, and blame usefully. Conflicts are structurally impossible under normal workflows (append-only + file-per-artifact). -
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.
-
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
concernin five seconds; an agent finishing a task can leave acommentto flag a follow-up; a scanner can drop asecurity-advisoryinto 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. -
Deterministic record IDs. A record's
idis 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. -
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.
-
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.
-
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 viasupersedes, and into graphs viadependencyrecords. -
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 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:
- Rename the
.qualfile to match the new subject name. - Update dependency records to reference the new name wherever the old name
appeared (both as
subjectand independs_onarrays). - Note: Existing records inside the renamed
.qualfile still contain the oldsubjectfield in their JSON. Since record IDs are content-addressed, changing thesubjectfield would change the ID, breaking supersession chains.
The RECOMMENDED workflow after a rename is:
- Rename the
.qualfile and update dependency records. - Run
qualifier compact <new-name> --snapshotto collapse history into a fresh epoch under the new name. - 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.
"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
// 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
endis absent, it is set equal tostart. - If
colis absent from a position, it remains absent (not defaulted). content_hashis 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:
- Read the file identified by the record's
subject. - Extract lines
start.linethroughend.line(inclusive, 1-indexed). Columns are ignored — full lines are always hashed. - Join the extracted lines with
\n(no trailing newline). - Compute the BLAKE3 hash of the resulting byte string.
- 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:
{"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:
{"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:
-
Normalization. Before serialization:
typeMUST be materialized. If absent, set to"annotation".metaboxMUST be materialized. If absent, set to"1".span.endMUST be materialized (in body). If absent, set equal tospan.start.idMUST be set to""(the empty string).
-
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. -
Body field order. Body fields MUST appear in lexicographic (alphabetical) order. Nested objects (like
span) also have their fields in lexicographic order. -
Absent optional fields. Optional fields whose value is absent (null, None, etc.) MUST be omitted entirely.
tagsMUST be omitted when the array is empty. Theidfield is the sole exception — it is always present (set to""). -
Whitespace. No whitespace between tokens. No space after
:or,. No trailing newline. The output is a single compact JSON line. -
No trailing commas. Standard JSON — no trailing commas.
-
String encoding. Standard JSON escaping (RFC 8259 Section 7). Implementations MUST NOT add escapes beyond what JSON requires.
-
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 for the full MCF definition.
2.8.2 Example
Given an annotation with no optional body fields, the MCF is:
{"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:
{"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
subjectfield. Cross-subject supersession is forbidden. Implementations MUST reject it. - The
spanfield 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
supersedesreferences (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):
{"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
referencesvalue 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.rsMAY reference a record onsrc/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
". - 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:
{"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):
{"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:
{"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:
- Pruning all superseded records. If record B supersedes A, only B is retained. The entire chain collapses to its tip.
- Optionally snapshotting. When
--snapshotis 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
.qualfile. No special reader support is needed. qualifier compact --dry-runMUST 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:
{"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:
{"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:
{"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:
{"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:
{"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 predicates for use with DSSE signing and Sigstore distribution.
Mapping (annotation):
{
"_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
--issueris omitted, defaults to the VCS user identity (see §8.4). - When a span is given,
content_hashis 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 optionaldetail,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
# 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.
// 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
.qualfiles 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 jsononshowandlscommands. - Batch annotation:
qualifier record --stdinreads JSONL from stdin (overrides objects or full records). For non-annotation record types,qualifier emit --stdinaccepts complete records. - Suggested fixes: The
suggested_fixbody field gives agents a concrete action to take. - Span precision: The
spanbody field lets agents target specific line ranges, making annotations actionable without hunting for the relevant code. - Filtering by kind:
qualifier ls --kind blocker --format jsongives 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
referencesfield 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:
-
.gitignore— Standard Git ignore files, including:.gitignorefiles at any level of the tree.git/info/exclude(per-repo excludes)- The global gitignore file (e.g.,
~/.config/git/ignore) .gitignorefiles in parent directories above the project root (matching Git's own behavior in monorepos)
-
.qualignore— A qualifier-specific ignore file using the same syntax as.gitignore. Place a.qualignorefile anywhere in the tree to exclude paths from qualifier's discovery walk. Useful for ignoring vendored code, generated files, or example directories that have.qualfiles 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
[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 graphfor 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 signto 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.qualfile 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.