The RAC (Requirements as Code) Specification
Version 0.1.0
1. Abstract
RAC (Requirements as Code) is a format for typed, validated, versioned product
knowledge — requirements, decisions, roadmaps, prompts, and designs — stored as
Markdown files in a Git tree. This specification defines the artifact model,
the closed status lifecycle, the closed typed-relationship vocabulary, and
the validation and conformance rules a corpus and its tools must satisfy. It is
written for implementers of producers (tools that write RAC corpora) and
consumers (tools that read, validate, or serve them).
2. Status of this document
This is v0.1.0 of the RAC specification, extracted from the reference
implementation, asdecided-core. Every
normative statement below is traceable to behavior the reference validator
enforces today (the machine-checked trace lives in extraction-inventory.json;
one deliberate exception is called out in §10). The specification is pre-1.0:
expect breaking changes before v1.0, governed by the compatibility policy in
§10. The specification versions independently, decoupled from asdecided-core's
release versioning.
3. Motivation, goals, and non-goals
Teams rule mistakes out by recording decisions. When the record lives in unstructured prose, or is never written, a coding agent cannot reliably apply it and re-introduces the mistake. RAC makes the record a typed, machine-validated corpus: every artifact has a type, an identity, a lifecycle status, and typed links to the artifacts it relates to, and a CI gate rejects corpus states that would mislead a reader, human or agent, before they land.
Goals.
- A deterministic, offline-checkable definition of a valid corpus: the same corpus bytes always produce the same findings and the same exit code.
- Prescriptive-knowledge semantics: a closed lifecycle with supersession, and machine-readable relationship types with referential integrity.
- Implementation independence: everything here is checkable from the files
alone, with no dependency on
asdecided-core, a particular implementation language, or any serving layer.
Non-goals.
- Not a general knowledge format. Portable descriptive knowledge — tables, metrics, runbooks — is OKF's job (§5); RAC composes with OKF rather than replacing it.
- Not a retrieval or RAG system. RAC defines what a corpus is, not how it is indexed, embedded, or served.
- Not a project-management or ticketing replacement. Status is knowledge currency (is this decision still current?), never work or delivery state; external tickets are referenced, not modeled.
- Not a prescription of MCP or any particular serving layer. The reference implementation ships an MCP server; nothing here requires one.
4. Terminology
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
- Artifact — one Markdown file carrying one unit of typed product knowledge: an optional YAML frontmatter envelope plus a structured body.
- Corpus — the set of artifacts under a corpus root directory, together
with its root configuration file (
.rac/config.yaml). - Type — one of the closed set of artifact kinds (§6.1). A document matching no type is untyped ("unknown") — a valid outcome, not an error.
- Status — an artifact's lifecycle value, drawn from its type's closed enum (§7). Statuses partition into live and retired.
- Typed link — a reference from one artifact to another (or to an external target), declared in a relationship section whose name comes from the closed vocabulary in §8.
- Supersession — the lifecycle event in which an artifact is retired and,
typically, replaced; the replacing decision points at the retired one via
the
supersedesedge (§7.2). - Finding — one validation result, identified by a stable machine code, with an intrinsic severity and an enforcement class (§9).
- Severity — a finding's intrinsic annotation level:
error,warning, orinfo(§9.1). Independent of whether the finding blocks. - Enforcement class — whether a finding blocks the gate:
blockingoradvisory(§9.1). The exit signal fails iff a blocking finding is present. - Enforcement policy — the corpus-committed mapping (in
.rac/config.yaml) that MAY reclassify a finding's enforcement class or suppress it (§9.5). A mechanism, not part of spec conformance. - Default policy — the enforcement classes with no policy overrides present (§9.3). Spec conformance is always evaluated under the default policy (§9.7); an unqualified conformance claim means the default policy.
- Gate — the composed enforcement run (structural validation, relationship integrity, and review) whose exit signal a CI check consumes (§9.4).
- Tier — a finding's review priority level, 1 (highest impact) through 6 (§9.2).
- Producer — a tool that writes artifacts or corpora. Consumer — a tool that reads, validates, or serves them.
5. Relationship to OKF
RAC uses the same carrier as Google's Open Knowledge Format (OKF, v0.1 Draft): a Git tree of Markdown files with YAML frontmatter. The relationship is mechanical and one-directional:
- A RAC corpus is expressible as a conformant OKF v0.1 bundle. A conformant
producer MUST be able to emit the derived bundle: one file per typed
artifact whose OKF
typemaps from the RAC type (requirement→Requirement,decision→ADR,design→Design,roadmap→Roadmap,prompt→Prompt), plus generatedindex.md/log.mdentry points. A registered RAC type without an OKF mapping is a conformance error (okf-unmapped-type), never a silent omission. - RAC's other envelope keys (
schema_version,id,relationships,tags) ride as OKF producer-defined keys, which OKF consumers are directed to preserve and not reject (OKF §9), so the export is lossless. RAC does not project a frontmattertitleordescription; both are OKF-optional, and an OKF consumer without one MAY fall back to the filename (OKF §4.1). A producer MAY project the H1 astitlefor better display in permissive consumers;conformance/conformance.mdrecords the parser-compatibility caveat this raises. - RAC identity is the explicit
idfield, not the file path (§6.4): a RAC-aware consumer MUST resolve links by identifier. OKF's path-as-identity does not survive a file rename; RAC's does. - The reverse direction is not defined. An arbitrary OKF bundle has untyped
links, an open
typeset, and no lifecycle, so it cannot in general become a valid RAC corpus without human classification. Conversion is specified only from the strict layer to the permissive carrier.
A typed artifact MUST NOT be named index.md or log.md
(okf-reserved-filename-collision) — those collide with the generated bundle
entry points; an untyped document at those paths is a legitimate entry point.
6. Artifact model
6.1 Types
The artifact type set is closed:
requirement · decision · roadmap · prompt · design
A consumer MUST NOT invent types, and a producer MUST NOT emit a frontmatter
type outside this set (invalid-metadata-field). A document that declares
no type and matches no type structurally is untyped: consumers MUST treat it
as a valid, skipped document — never validate it against a type it does not
have, and never fail the corpus because of it.
Classification is deterministic and structural: a document is scored against
each type by the ## sections it contains (required sections count 1 point,
recommended ½; fit = points ÷ ceiling), and it classifies as the best-fit type
when fit ≥ 0.5 and at least one required section matched. Classification is
separate from validation: an invalid but recognizable artifact still
classifies as its type, then fails validation.
6.2 File format
An artifact is one UTF-8 Markdown file: an OPTIONAL leading YAML frontmatter
block delimited by --- (the closer MAY be --- or ...), then a CommonMark
body. The body MUST contain exactly one top-level # title
(missing-title / multiple-titles). Structure below the title is expressed
in ## sections, matched case-insensitively and whitespace-trimmed.
A corpus is discovered as the sorted set of *.md files under the corpus
root, recursively, skipping any path component that starts with .. No
directory layout is normative; decisions/, requirements/ and similar are
conventions only.
6.3 The frontmatter envelope
The frontmatter field set is closed:
| Field | Type | Presence | Rule |
|---|---|---|---|
schema_version |
integer | REQUIRED when frontmatter is present | MUST be a supported version (this spec: 1); anything else is unsupported-schema-version |
id |
string | RECOMMENDED | MUST match the ID grammar (§6.4); normalized uppercase |
type |
string | RECOMMENDED | MUST be a registered type (§6.1) |
relationships |
map | OPTIONAL (reserved) | kind → list of ID strings; shape-validated; not yet consumed (§8.6) |
tags |
list | OPTIONAL | non-empty strings; descriptive labels only |
An unknown frontmatter field is an error (invalid-metadata-field), never
ignored. Malformed YAML, duplicate keys, YAML aliases, nesting beyond 32
levels, and blocks over 64 KiB are all errors (malformed-frontmatter,
duplicate-frontmatter-key). A document with no frontmatter block at all is
legal (the legacy form). The envelope carries no timestamp fields: recency is
derived from version control, never stored in frontmatter.
---
schema_version: 1
id: RAC-01JY4M8X2QZ7
type: decision
---
# ADR-004: Parser Strategy
Product reasoning — status, context, decisions, requirements — lives in body sections, and MUST NOT migrate into the envelope. The envelope is machine-operational metadata only.
6.4 Identity
An artifact ID is <KEY>-<SUFFIX>: a repository key matching
^[A-Z][A-Z0-9]{1,9}$ (declared once in the corpus config, §6.5), a hyphen,
and a 12-character Crockford base32 suffix (uppercase, no I/L/O/U):
^[A-Z][A-Z0-9]{1,9}-[0-9A-HJKMNP-TV-Z]{12}$ e.g. RAC-01JY4M8X2QZ7
IDs are matched case-insensitively and normalized to uppercase. Generation is offline and branch-safe (an 8-character millisecond-timestamp segment plus a 4-character random segment); no allocation server exists or is permitted.
Identity resolution precedence (first match wins): the frontmatter id;
an explicit ## ID section value; a <letters>-<digits> filename-stem prefix
(e.g. adr-004 from adr-004-parser-strategy.md); the whole filename stem. An
artifact answers to all of these as resolution aliases, so human-readable
references (ADR-004) keep resolving after it adopts a canonical ID. The
document title is never an identifier, and inline [REQ-NNN] lines are not
identifiers — link targets are whole artifacts.
Three normative consequences:
- Identity survives file moves. The canonical identity is the declared
id, never the path. Renaming or moving a file MUST NOT change the artifact's identity, and inbound references MUST keep resolving. - Uniqueness is corpus-wide on the canonical identifier.
duplicate-artifact-identifieris a blocking finding. Aliases never create duplicates on their own. - Conflicts are never silently resolved. A frontmatter
idthat differs from a declared legacy identity isconflicting-identity(error); a consumer MUST NOT pick one.
6.5 The corpus configuration
A corpus root holds .rac/config.yaml, discovered by walking upward from the
working directory. It carries machine policy, not knowledge:
repository_key (REQUIRED for ID generation), rac_spec (the spec-version
declaration, §10.1), and OPTIONAL ticketing.provider, validation severity
overrides, and enforcement policy (§9.5).
6.6 Sections per type
For each type: required sections MUST be present (a missing one is the
blocking finding missing-<section>); recommended sections never fail
(their absence is a tier-4 advisory, §9.2); optional sections are recognized
and extracted but never scored and never reported missing.
| Type | Required sections | Recommended (in priority order) |
|---|---|---|
requirement |
Problem, Requirements | Success Metrics, Risks, Assumptions |
decision |
Context, Decision, Consequences | Status, Category, Alternatives Considered |
roadmap |
Outcomes, Initiatives | Success Measures, Assumptions, Risks |
prompt |
Objective, Input, Instructions, Output | Constraints, Examples, Evaluation |
design |
Context, User Need, Design, Constraints | Rationale, Alternatives, Accessibility, Style Guidance, Open Questions |
Each type's optional sections are exactly its relationship sections (§8.2)
plus, for decision, Supersedes. Recognized heading synonyms (e.g.
Success Criteria → Success Metrics) aid classification only; validation
expects the canonical headings.
6.7 Requirement lines
Inside a requirement artifact's ## Requirements section, each non-empty
line MUST be a well-formed requirement statement:
[REQ-<digits>] <non-empty description>
A line with no bracket prefix is req-missing-id; a malformed bracket ID is
malformed-req-id; an empty description is empty-req-text; a duplicate
REQ ID within the file is duplicate-req-id — all blocking. Requirement-ID
uniqueness is per-file.
Normative language inside requirement lines is disciplined per BCP 14: a
lowercase or mixed-case shall/must/should is ambiguous and blocking
(requirement-normative-keyword), because only ALL-CAPS keywords carry
normative weight.
Not specified. The reference implementation also emits advisory ISO/IEC/IEEE 29148 "singular requirement" and EARS response-clause findings (
requirement-not-singular,requirement-non-ears,requirement-ears-clause; ADR-056). These are authoring heuristics rather than conformance rules and are excluded from this specification. Only the BCP-14 casing rule above is normative.
## Requirements
- [REQ-001] The exporter MUST emit one bundle entry per typed artifact.
- [REQ-002] If the corpus is empty, then the exporter SHALL emit an empty index.
6.8 Machine-readable schemas
The JSON Schemas in schema/ are the normative machine-readable form of
§6.3–§6.7: schema/frontmatter.schema.json for the envelope,
schema/artifact.schema.json for the parsed per-type structural contract.
They are documentation for implementers, derived from validator behavior — the
reference implementation itself ships no JSON Schema and takes no
schema-validation dependency, so these are not an asdecided-core artifact. Prose
and schema are cross-checked against one extraction inventory; a disagreement
between them is a defect in this specification, to be resolved by issue
rather than by following either side. Prose-only specs drift: OKF shipped
prose-only and its own reference parser diverged from the text within days
(it requires four frontmatter fields where the text requires one; see
conformance/conformance.md).
7. The status lifecycle
Status is declared in a ## Status body section — the first non-empty line is
the value, matched case-insensitively. Status is OPTIONAL: a missing section
is always legal. A present value MUST belong to the type's closed enum; an
unknown value is the blocking finding invalid-<type>-status.
| Type | Live values | Retired values |
|---|---|---|
requirement |
Proposed, Accepted | Superseded, Deprecated |
decision |
Proposed, Accepted | Superseded, Deprecated |
design |
Proposed, Accepted | Superseded, Deprecated |
roadmap |
Planned, Achieved | Superseded, Abandoned |
prompt |
Active | Deprecated |
Achieved (roadmap) is a live terminal state: the intent was delivered, the
record remains valid to reference. Retirement marks replaced or dropped
knowledge, not completed work — status is knowledge currency, never delivery
tracking.
No state-machine transitions are enforced: any enum value may be written at any time, and history lives in version control. The lifecycle's force comes entirely from the live/retired partition:
7.1 Two other constrained enums
decision MAY declare ## Category (Architecture, Product, Process,
Technical, Other; unknown value → invalid-decision-category). roadmap MAY
declare ## Horizon (now, next, later, or a quarter matching
Q[1-4] YYYY; unknown value → invalid-roadmap-horizon). Both blocking.
7.2 Supersession semantics
When an artifact carries a retired status:
- A live artifact MUST NOT reference it through any relationship except
supersedes— such a reference is the findingrelationship-target-superseded, blocking by default (§9.3). - The
supersedesedge is exempt by design: the replacing decision legitimately points at the decision it retires. - References from retired artifacts are exempt: historical chains stay intact.
- A consumer MUST NOT present a retired artifact as current knowledge (conformant-consumer rule, §9.6).
The retired decision (RAC-01JYAAAAAAAA) sets ## Status to Superseded;
the replacing decision names it in its own body, the one edge allowed to point
at a retired target:
## Supersedes
- RAC-01JYAAAAAAAA
8. Typed relationships
8.1 Design position
OKF §5.3 states that the specific kind of relationship a link asserts "is conveyed by the surrounding prose, not by the link itself," and that consumers must tolerate broken links. RAC inverts both choices: relationship semantics are machine-readable, and broken links are blocking. An agent cannot be relied on to infer an edge type from prose, so for the links RAC recognizes (§8.2) the type is part of the declaration itself, and a reference that fails to resolve is a finding rather than tolerated ambiguity.
8.2 The vocabulary
Relationships are declared as body sections whose names come from a closed vocabulary. One reference per non-empty line; a leading list marker is stripped; otherwise the line text is the reference, preserved verbatim.
| Edge | Declared by | Target (range) | Direction | Validation |
|---|---|---|---|---|
related_requirements |
all five types | requirement |
undirected | resolve + range + status |
related_decisions |
all five types | decision |
undirected | resolve + range + status |
related_roadmaps |
all five types | roadmap |
undirected | resolve + range + status |
related_prompts |
requirement, roadmap, design | prompt |
undirected | resolve + range + status |
related_designs |
requirement, decision, roadmap, prompt | design |
undirected | resolve + range + status |
supersedes |
decision | decision |
directed (inverse superseded-by), acyclic |
resolve + range; exempt from the retired-target rule |
related_tickets |
all five types | external ticket key/URL | undirected | format-linted against the configured provider; never resolved |
verified_by |
requirement | external test/trace path | directed (inverse verifies) |
recorded; not resolved, not existence-checked |
applies_to |
decision | repo path, glob, or component | directed (inverse governed_by) |
literal path entries existence-checked against the repo root |
Section headings map to edge names by lowercasing and replacing spaces with
underscores (## Related Decisions → related_decisions). Cardinality is
many-valued for every edge. Edge legality by source type ("declared by") is
enforced: a recognized edge populated on a type that does not declare it is
relationship-edge-unsupported and produces no edge.
Recognition boundary. Relationships are recognized by name, against this
closed vocabulary. A heading outside it — ## Blocks, ## Related Widgets —
is not a relationship: it carries no edge and no finding, like any prose
section (§11). A deterministic offline parser cannot distinguish an intended
but unrecognized edge from ordinary prose, so validation covers the edges the
vocabulary names, not every relationship an author might imply. An unknown
relationship kind is rejectable only where the kind is an explicit key: the
reserved frontmatter relationships map (§8.6), never a body heading.
## Related Decisions
- RAC-01JYBBBBBBBB
- ADR-016
8.3 Referential integrity
Every non-external reference MUST resolve, uniquely, to another artifact in the corpus, via the identity aliases of §6.4:
- no match →
relationship-target-not-found(blocking); - more than one match →
relationship-target-ambiguous(blocking); - resolves to the declaring artifact →
relationship-self-reference(blocking by default).
A resolved target MUST be of the edge's range type
(relationship-target-type-mismatch, blocking); an untyped target is not a
range violation. A resolved live-to-retired reference outside supersedes is
relationship-target-superseded (§7.2).
8.4 Graph shape
The supersedes graph MUST be acyclic. A cycle — reported once per strongly
connected component — is relationship-cycle (blocking). Undirected
related_* edges carry no shape constraint.
8.5 External edges
related_tickets, verified_by, and applies_to target things outside the
corpus, so they are exempt from resolution, range, and status checks. Ticket
references are format-linted offline against the corpus-configured provider
(jira, github, linear, azure-devops, servicenow, or none;
malformed entry → malformed-ticket-reference, blocking). applies_to
literal path entries (containing /, no glob characters) MUST exist relative
to the repository root (applies-to-target-not-found, blocking); glob and
component-label entries are recorded without checking. The engine never
contacts an external system: all checks are pure functions of the corpus and
the working tree.
8.6 Reserved: frontmatter relationships
The frontmatter relationships map (§6.3) is reserved for a future migration
of relationship declarations into the envelope. Producers MAY write it (shape:
edge name → list of canonical IDs); consumers MUST parse and preserve it but
MUST NOT yet treat it as the source of relationship truth — the body sections
are authoritative.
9. Validation, severity, and conformance levels
9.1 The finding model
Every check has a stable machine code. A finding carries two independent classifications:
- Intrinsic severity —
error|warning|info. This drives the annotation level in reports (SARIF:error/warning/note). - Enforcement class —
blocking|advisory. This drives the exit signal: a gate run fails if and only if at least one blocking finding is present. The two are independent: a warning-severity finding can be blocking (relationship-target-supersededis).
All findings MUST be reported; advisory findings MUST NOT affect conformance or the exit signal.
9.2 Review tiers
Aggregated review classifies findings into six priority tiers. Tiers 1 and 2 render a corpus non-conformant; a conformant CI gate MUST fail on any tier 1 or tier 2 finding. Tiers 3–6 MUST be reported but MUST NOT affect conformance.
| Tier | Name (as in the reference implementation) | Code | Fails? |
|---|---|---|---|
| 1 | Invalid artifact (validation errors) | invalid-artifact |
yes |
| 2 | Broken relationship | broken-relationship |
yes |
| 3 | Missing required information (unrecognized artifact) | unknown-artifact |
no |
| 4 | Missing recommended information | missing-recommended-sections |
no |
| 5 | Stale corpus (write-cadence nudge; opt-in) | stale-corpus |
no |
| 6 | Suspect drift (referenced target changed later) | suspect-artifact |
no |
Tiers 5 and 6 are advisory-only extensions that give version-control-derived hygiene signals a reporting slot without ever gating.
9.3 The normative check table
Every specified check, with its intrinsic severity and default enforcement
class. This table is generated from extraction-inventory.json (checks) and
MUST stay in lockstep with it.
Structural validation (source: validate) — errors are blocking, warnings
and info advisory:
| Code | Severity | Enforcement |
|---|---|---|
artifact-oversize, unreadable-artifact |
error | blocking |
malformed-frontmatter, duplicate-frontmatter-key, invalid-metadata-field, unsupported-schema-version, invalid-id-syntax, conflicting-identity |
error | blocking |
missing-title, multiple-titles |
error | blocking |
missing-<required-section> (per type, §6.6) |
error | blocking |
invalid-requirement-status, invalid-decision-status, invalid-decision-category, invalid-roadmap-status, invalid-roadmap-horizon, invalid-prompt-status, invalid-design-status |
error | blocking |
req-missing-id, empty-req-text, malformed-req-id, duplicate-req-id |
error | blocking |
requirement-normative-keyword |
error | blocking |
malformed-ticket-reference |
error | blocking |
okf-unmapped-type, okf-reserved-filename-collision |
error | blocking |
non-utf8-content, field-truncated, body-truncated |
warning | advisory |
roadmap-no-advancement-link |
warning | advisory |
missing-success-metrics, missing-risks, empty-problem, too-many-requirements, duplicate-req-text, ambiguous-verb |
warning | advisory |
Relationship integrity (source: relationships) — every finding is blocking
by default, regardless of intrinsic severity:
| Code | Severity | Enforcement |
|---|---|---|
duplicate-artifact-identifier |
error | blocking |
relationship-target-not-found |
error | blocking |
relationship-target-ambiguous |
error | blocking |
relationship-target-type-mismatch |
error | blocking |
relationship-cycle |
error | blocking |
applies-to-target-not-found |
error | blocking |
relationship-target-superseded |
warning | blocking |
relationship-self-reference |
warning | blocking |
relationship-edge-unsupported |
warning | blocking |
Review (source: review) — tiers 1–2 blocking, 3–6 advisory:
| Code | Tier | Severity | Enforcement |
|---|---|---|---|
invalid-artifact |
1 | error | blocking |
broken-relationship |
2 | warning | blocking |
unknown-artifact |
3 | info | advisory |
missing-recommended-sections |
4 | warning | advisory |
stale-corpus |
5 | info | advisory |
suspect-artifact |
6 | warning | advisory |
9.4 The gate and its exit contract
The gate composes structural validation (including OKF conformance), relationship integrity, and review over one corpus snapshot, applies the corpus enforcement policy (§9.5), and reports one deterministic, sorted finding list. Exit contract:
| Exit | Meaning |
|---|---|
| 0 | no blocking finding |
| 1 | at least one blocking finding |
| 2 | usage or I/O error |
A conformant CI integration MUST fail its check exactly when the gate exits
non-zero, and MUST NOT reinterpret findings or decide independently what
blocks: enforcement classification belongs to the engine under the committed
corpus policy. Reports MUST be deterministic — the same corpus bytes and
the same committed configuration yield a byte-identical report (stable sort,
no timestamps). In SARIF output the level encodes the intrinsic severity
(error/warning/note); the enforcement class is carried solely by the
exit code.
9.5 Policy: overrides are a governed, committed mechanism
The classifications in §9.3 are the default policy — the enforcement
classes with no overrides present. A corpus MAY tune them in its committed
configuration: per-rule/per-type severity overrides (validation: — downgrade
error → warning, or off to suppress) and per-code enforcement
reclassification (enforcement: — blocking / advisory / off; precedence
off > blocking > advisory > default). This is a mechanism, not part of
spec conformance: an override changes the local gate verdict, never what §9.3
specifies. Because the policy is a committed file, the same repository state
always yields the same findings and exit code, and a tool MUST NOT apply
enforcement policy from any source outside the corpus. The policy governs the
gate verdict only: it never relaxes the consumer obligations in §9.6, which
hold regardless of any local reclassification.
9.6 What a conformant consumer MUST reject
OKF §9 enumerates what consumers must not reject over — unknown types, missing optional fields, broken cross-links, unknown keys, missing indexes. RAC's strict mirror inverts that list. A conformant consumer MUST reject (refuse to treat as a valid corpus):
- any tier 1 or tier 2 finding — equivalently, any blocking finding under the corpus's committed policy;
- any unknown
statusvalue; and any populated relationship section that uses a recognized edge name on a type that does not declare it (relationship-edge-unsupported) — fail, do not guess. (A heading outside the vocabulary is prose, not a relationship (§8.2), and is out of scope here.) - any corpus declaring a spec version newer than the consumer supports
(§10.3) — and, per artifact, any
schema_versionoutside the supported set.
Separately from corpus rejection, and independent of the corpus's committed
policy: a conformant consumer MUST NOT present a retired artifact as current
knowledge (§9.7). This is a per-artifact presentation rule for the serving
layer, not a corpus-validity condition. A corpus MAY downgrade
relationship-target-superseded so a live-to-retired reference passes its
gate (§9.5, a common migration posture); the reference still MUST be
surfaced as pointing at retired knowledge, never as current. The two actors
differ: the gate decides whether a corpus state may land, the consumer
decides how landed knowledge is presented, and only the first is
policy-tunable.
This inverts OKF's permissive consumption model because the two formats carry different kinds of knowledge. Stale descriptive knowledge degrades output quality gradually; stale prescriptive knowledge reverses a recorded decision outright. A consumer that guesses on an unknown value produces output that cannot be told apart from grounded knowledge, so failing is the safer behavior.
9.7 Conformance levels
Conformance is always evaluated under the default policy (§9.5): local severity overrides and enforcement reclassifications change a repository's own gate verdict, never its spec conformance. This keeps conformance claims comparable across repositories. A claim MUST name the policy it was evaluated under; an unqualified claim means the default policy.
- Conformant corpus — under the default policy, structural validation
(§9.3,
validatesource) yields zero blocking findings, and every artifact-level rule in §6–§7 holds. - Gated corpus — a conformant corpus whose full gate run (§9.4) yields
zero blocking findings under the default policy: link integrity holds, no
live artifact references a retired one outside
supersedes, and review tiers 1–2 are empty. - Conformant producer — emits only artifacts that are (individually and jointly) a conformant corpus, and never emits an enum value, frontmatter field, or edge name outside this specification (extend via §11, never via enum values).
- Conformant consumer — resolves identity per §6.4, preserves the extension content it round-trips (§11), refuses everything in §9.6, and MUST NOT present a retired artifact as current.
10. Versioning and compatibility policy
This specification uses semantic versioning, independent of any implementation's release scheme.
10.1 Corpus-level spec-version declaration
Every corpus MUST declare the specification version it targets, in one
machine-readable location: the rac_spec key of the corpus root
configuration, .rac/config.yaml:
repository_key: RAC
rac_spec: "0.1"
rac_spec versus schema_version. These are two different version axes
and MUST NOT be conflated. rac_spec (corpus-level, this section) declares
which version of this specification — the whole contract of §6–§11 — a
corpus targets. schema_version (per-artifact frontmatter, §6.3) declares
which version of the frontmatter envelope an individual artifact uses. Each
spec version states which envelope versions it accepts: spec v0.1 accepts
schema_version: 1, and no other. A future spec version MAY accept
additional envelope versions; that mapping is part of the spec contract, so a
consumer resolves envelope compatibility only after it has accepted the
declared rac_spec (§10.3).
The declaration lives in the corpus config, uniformly, with no exceptions.
OKF §11 puts its okf_version declaration in the frontmatter of index.md,
a file OKF otherwise defines as frontmatter-free (§6); a single-file
exception of that kind invites parser bugs, and this specification does not
copy it.
Implementation status. This subsection is the one requirement in this specification not yet enforced by the reference implementation:
asdecided-coredoes not currently readrac_specor refuse newer-versioned corpora (§10.3). An unknown corpus-config key is a harmless no-op, so declaringrac_spectoday breaks nothing. A conformant implementation MUST implement both; the reference implementation's progress toward it is tracked in its CHANGELOG, not here.
10.2 What may change when
- New enum values (a
statusvalue, a relationship type, an artifact type) MAY be added in a minor version. - Removing or changing the meaning of an existing enum value, changing an existing check's tier or default enforcement class, or tightening a MUST, REQUIRES a major version.
- New advisory checks MAY be added in a minor version; new blocking checks REQUIRE a major version.
- Editorial changes and new examples are patch versions.
10.3 Version-mismatch behavior
A consumer encountering a declared spec version newer than it supports MUST fail with a diagnostic naming both versions ("corpus targets 0.3, consumer supports 0.1"); it MUST NOT attempt best-effort interpretation. OKF §11 pairs its version declaration with a SHOULD for best-effort consumption by consumers that do not understand the version, so the declaration carries little weight there. RAC pairs the same kind of declaration with mandatory refusal. What separates the two formats is the failure behavior, not the field.
The rationale is diagnosability: strict unknown-value handling (§9.6) is only
actionable if a consumer can distinguish a corrupt corpus from one targeting a
newer spec. Strict ecosystems pair failure with a version field for this
reason (JSON Schema's $schema, OpenAPI's version). Accordingly, an
unknown-enum error MUST include the declared spec version, so it reads
"corpus targets 0.3, consumer supports 0.1" rather than an opaque rejection.
11. Extension points
Producers extend RAC via new keys and sections, never new values in the closed enums:
tagscarries arbitrary descriptive labels — this is the extension surface inside the frontmatter envelope. The envelope's field set is otherwise closed (§6.3): a producer MUST NOT add its own frontmatter keys (invalid-metadata-field). A namespaced extension-key scheme MAY be specified in a future minor version; until then, structured extension data belongs in body sections.- Non-vocabulary body sections are free: any
##section outside a type's required/recommended/optional set is carried, searchable content with no validation semantics. - Consumers MUST preserve extension content they round-trip (
tags, therelationshipsmap, free body sections) so the OKF export stays lossless. - What extension MUST NOT do: add a
statusorcategoryvalue, a relationship section name, or an artifact type. Those are enum changes and follow §10.2.
Appendix A — Minimal complete corpus
The smallest corpus that validates with zero blocking findings — config with
the spec-version declaration, one decision, one requirement, one typed link —
also shipped (recommended sections filled in) as examples/minimal-corpus/:
minimal/
├── .rac/
│ └── config.yaml
├── record-decisions-as-adrs.md
└── decision-capture.md
.rac/config.yaml:
repository_key: MIN
rac_spec: "0.1"
record-decisions-as-adrs.md:
---
schema_version: 1
id: MIN-01JYX0000001
type: decision
---
# ADR-001: Record decisions as ADRs
## Status
Accepted
## Context
Decisions were scattered across chat and memory, so the team repeated
debates it had already settled.
## Decision
Every significant decision is recorded as an ADR in this corpus.
## Consequences
Decisions become citable and supersedable; writing them takes effort.
## Related Requirements
- MIN-01JYX0000002
decision-capture.md:
---
schema_version: 1
id: MIN-01JYX0000002
type: requirement
---
# Decision capture
## Status
Accepted
## Problem
Product decisions are invisible to the tools that need them.
## Requirements
- [REQ-001] The team MUST record each significant decision as one decision artifact.
## Related Decisions
- MIN-01JYX0000001
(Recommended sections are omitted here for brevity — their absence is advisory, never blocking.)
This specification is maintained at
asdecided/spec under the
process in CONTRIBUTING.md and GOVERNANCE.md. The reference implementation
is asdecided/core.