# Record rules

<!-- GENERATED from packages/gavr/src/rules.ts by `pnpm --filter @gadvisory/gavr spec:generate`. Do not edit by hand. -->

Every cross-field rule a GAVR 1.0 record must satisfy, and
whether the published JSON Schema carries it.

**Validating against the schema is necessary and not sufficient.** The gap is
asymmetric, which is what makes it dangerous: an implementation that checks only
the schema ACCEPTS records the registry refuses, so it looks conformant right up
until submission fails. The rules below marked as companion rules are exactly
that gap. Implement them.

Each has at least one case in the [conformance corpus](../conformance/record.json),
and this package's own tests run that corpus, so the split below is a verified
property rather than a claim.

## Companion rules — you must implement these yourself

17 of 49.

### GAVR-R-004

At most one identifier may carry the PRIMARY role.

Applies to: `identifiers`

Not expressible because an upper bound on how many array items match a subschema needs `maxContains`, which arrived in draft 2019-09, and the published schema targets draft-07 for validator availability outside JavaScript.

### GAVR-R-005

The PRIMARY identifier, if present, must equal the record's own id.

Applies to: `identifiers`

Not expressible because it compares an array item against a sibling scalar elsewhere in the document, and JSON Schema has no cross-reference between two instance locations.

### GAVR-R-006

At most one identifier may be CANONICAL for a given namespace and environment.

Applies to: `identifiers`

Not expressible because it groups array items by a key computed from two of their own properties, and JSON Schema cannot partition an array before counting it.

### GAVR-R-007

Every namespace present in the production identifier set must retain at least one citable identifier (PRIMARY or CANONICAL).

Applies to: `identifiers`

Not expressible because it is the same grouping problem as GAVR-R-006, plus a per-group existence check.

### GAVR-R-009

A numeric identifier segment (a GAVLR delegation segment or a GAVPR mint id) must be an exactly representable integer.

Applies to: `id`

Not expressible because a regex accepts a decimal run of any length, and a segment beyond 2^53 would resolve to a different registry than the one written down the moment any implementation parses it as a number.

### GAVR-R-011

A description list must contain no more than one primary entry.

Applies to: `cna.descriptions`

Not expressible because it is the `maxContains` gap described on GAVR-R-004.

### GAVR-R-012

No two descriptions in one list may share a language tag.

Applies to: `cna.descriptions`

Not expressible because `uniqueItems` compares whole items, so two entries differing only in `value` satisfy it while both claim to be the German text.

### GAVR-R-013

A markdown value must conform to the restricted profile: no raw HTML, no images, and link destinations limited to https:, http: and mailto:, with content inside fenced code blocks and code spans exempt.

Applies to: `*.descriptions[].value`

Not expressible because it requires scanning markdown structure to tell a live tag from one inside a fence, and a regex that ignored the fence exemption would refuse precisely the advisories about markup injection that most need rich text.

### GAVR-R-023

A container's role must be one its author's kind can hold: owner, peer and root are AUTHORITY, publisher is PUBLISHER, registry is REGISTRY, contributor is USER, regulator is REGULATOR, and translator and reviewer admit AUTHORITY, PUBLISHER, USER or TOOL.

Applies to: `*.role`

Not expressible because it constrains one property by the value of another inside a sibling object, which draft-07 can express only as an allOf of if/then pairs the generator does not produce from a Zod refinement.

### GAVR-R-047

An identifier's supersededBy must name a different identifier that this record carries.

Applies to: `identifiers[].supersededBy`

Not expressible because it compares one array item's value against the values of the other items, and JSON Schema cannot reference a sibling item.

### GAVR-R-038

Every container with role 'owner' must be authored by the record's own owner, or be one an ownership transfer explicitly retired or retained.

Applies to: `containers[].author`

Not expressible because It compares a container's `author` against the record's `owner`, two values in different parts of the same document, and consults a third (`ownershipHistory[].retires` and `.retains`). draft-07 has no way to require that one property equals another, nor to reference an array elsewhere in the document..

### GAVR-R-041

An ownership transfer must not retire every owner container: the incoming owner attaches its own in the same revision.

Applies to: `containers[]`

Not expressible because Whether a container is live depends on whether its `id` appears in `ownershipHistory[].retires`, a different array in the same document. draft-07 cannot express a cross-array reference..

### GAVR-R-040

An ownership transfer may only retire or retain a container the record actually carries.

Applies to: `ownershipHistory[].retires`

Not expressible because It requires every value in one array to match the `id` of some element of another array in the same document. draft-07 can express neither cross-array references nor uniqueness against a computed set..

### GAVR-R-042

The ownership chain must end at the record's declared owner.

Applies to: `ownershipHistory`

Not expressible because It compares the last element of an array against a property elsewhere in the same document. draft-07 can address neither the last element of an array of unknown length nor another property's value..

### GAVR-R-043

The ownership chain must be contiguous: every entry after the first takes over from the previous entry's recipient.

Applies to: `ownershipHistory[].from`

Not expressible because It compares each element against its predecessor. draft-07 has no way to reach a sibling element of the same array..

### GAVR-R-049

An ownership entry cannot both retire and retain the same container.

Applies to: `ownershipHistory[].retains`

Not expressible because It requires two arrays in the same object to be disjoint. draft-07 can constrain each array's items independently and has no way to compare one against the other..

### GAVR-R-044

Ownership entries must be in chronological order.

Applies to: `ownershipHistory[].at`

Not expressible because It compares each element's timestamp against its predecessor's, and the comparison is between instants rather than strings, since RFC 3339 permits an offset so two spellings of one instant do not sort lexicographically. draft-07 can do neither..

## Rules the published schema already carries

32 of 49. Listed so the catalogue is complete and so a
rule that moves between the two halves is visible in a diff, not so you have to
implement them: a stock draft-07 validator catches every one.

| Rule | Applies to | Requirement |
| --- | --- | --- |
| GAVR-R-001 | `containers[]` | A record in any state other than RESERVED must carry an owner container. |
| GAVR-R-002 | `disputedReason` | A disputed record must say what is disputed. |
| GAVR-R-003 | `withdrawnAt` | A withdrawn record must carry withdrawnAt. |
| GAVR-R-008 | `id` | The record's own id must be a well-formed GAVR, GAVPR or GAVLR identifier, within the network delegation depth cap. |
| GAVR-R-010 | `cna.descriptions` | A description list must contain an entry marked primary. |
| GAVR-R-014 | `*.affected[].identity` | An affected product needs at least one identity: purl, cpes, repository, product or packageName. |
| GAVR-R-015 | `*.affected[].ranges[]` | An unaffected range must carry a reason; 'not affected' and 'out of support, unassessed' are different claims. |
| GAVR-R-016 | `*.affected[].ranges[]` | A range with status affected-versions-unknown asserts that the range is not established, so it cannot carry bounds. |
| GAVR-R-017 | `*.severity[]` | A QUALITATIVE score must carry a qualitative value. |
| GAVR-R-018 | `*.severity[]` | A CVSS score must carry a vector string. |
| GAVR-R-019 | `*.severity[]` | An EPSS score must carry a numeric score. |
| GAVR-R-020 | `*.remediations[]` | An upgrade remediation must name the version that fixes it. |
| GAVR-R-021 | `*.remediations[]` | A patch-commit remediation must name the commit. |
| GAVR-R-022 | `*.detection[]` | A detection artifact needs either inline content or a url. |
| GAVR-R-024 | `*.descriptions[]` | The primary entry of a text list must be addressed to the public audience, so a surface with room for one description has one it may render. |
| GAVR-R-025 | `*.timeline[]` | A timeline entry with event 'other' must carry a note saying what happened. |
| GAVR-R-026 | `*.remediations[]` | A remediation of kind 'other' must carry a description; every other kind makes a specific claim and this one makes none. |
| GAVR-R-027 | `*.x_*` | A container property this specification does not define must be named x_something, and a consumer MUST preserve unknown x_ keys verbatim when re-serialising. |
| GAVR-R-028 | `*.intervention` | A role-scoped container field may appear only on the roles it belongs to. |
| GAVR-R-029 | `containers[]` | A container must carry the role-scoped block that defines its role: a peer states its standing, a publisher states its method. The two governance roles are exempt, because a root does not always intervene and a registry does not always reverse. |
| GAVR-R-030 | `*.intervention.basisNote` | An intervention with basis 'other' must say what the grounds were; without a note it records that a root acted and refuses to say why. |
| GAVR-R-031 | `*.intervention.appealable` | An intervention that is not appealable must say why: either the deadline that has passed or the appeal that has been decided. |
| GAVR-R-032 | `*.peerStanding.basisNote` | A peer standing of 'other' must say what the standing is; without a note it claims a relationship to the record and refuses to name it. |
| GAVR-R-033 | `*.enrichment.methodNote` | An enrichment method of 'other' must say how the claim was produced; the whole point of the field is that a consumer can weigh it. |
| GAVR-R-034 | `containers[]` | A container whose role does not assess the vulnerability cannot carry claims about it. A registry accepts and countersigns, a translator restates, a reviewer endorses, and a regulator acts under law; none of them assesses. |
| GAVR-R-035 | `*.contests[].disposition` | Only the record's owner may reject a container; any other party can dispute one. |
| GAVR-R-045 | `containers[]` | A root can only carry claims about the vulnerability inside an intervention: an accrediting body corrects a record by acting on it, with grounds and an appeal route, not by assessing it in passing. |
| GAVR-R-046 | `containers[].id` | A container carrying an intervention must carry an id: an appeal decision names what it decided, so a sanction nothing can point at is one no reversal or dismissal could ever be recorded against. |
| GAVR-R-036 | `redactions[].reason` | A redaction that reaches into a container must carry a reason that justifies overriding its author: leaked-credential, personal-data or legal-order. |
| GAVR-R-037 | `owner` | A record in any state but RESERVED must name its owner, so a container claiming that role has something to be checked against. |
| GAVR-R-039 | `containers[].review.note` | A review that is not a clean endorsement must say why. |
| GAVR-R-048 | `ownershipHistory[].reason` | An ownership entry that was not agreed to must say why the record was moved. |

## On the wire

The registry cites these ids when it refuses a document. `violations` is
**additive**: the refusal keeps its existing shape, and the rules arrive
beside the sentence rather than instead of it.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "record-refused: malformed-document: at most one identifier may be PRIMARY, found 2",
  "violations": [
    { "rule": "GAVR-R-004", "path": "identifiers",
      "message": "at most one identifier may be PRIMARY, found 2" }
  ]
}
```

Two things to read from it.

**`rule` is optional, and its absence means something.** A purely structural
failure — a missing required property, a wrong type — carries a `path` and a
`message` and no rule, because the published JSON Schema already expresses it
and your own validator caught it before you sent anything. A violation that
DOES carry a rule is therefore the interesting one: it is usually a companion
rule, which is exactly the class your validator passed.

**`violations` is absent entirely for a refusal that is not a schema failure**
— a bad signature, an unknown identifier, an out-of-scope record. Those have
their own reasons and no rule to cite.

`violationsTruncated: true` appears when the list was cut. It is reported
rather than applied silently, because a list that quietly stops reads as the
complete set of problems, and you would fix them, resubmit, and be refused
again for reasons nobody mentioned.
