Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,15 @@
"verification": "test",
"safety_goal": "SG-006",
"asil": "ASIL-B"
},
{
"id": "REQ-RELAY-100",
"title": "Generated version literals (spec document + CI enforcement)",
"text": "The specification document's own version literals -- the \"spec_version\" value in each JSON example (sec-12.1, sec-12.2, sec-12.4, sec-17.2, sec-20.6), the LABEL io.relay.spec-version value in sec-13.5's Docker example, and sec-19.4's \"Current version\" line and Go/C++/Rust snippets -- MUST match spec/version.json at every commit (sec-19.5). This repository's own CI MUST fail the build when any of them diverges, mirroring Requirement 14's obligation on a conformant implementation's own CI. Not a relay conform check: relay conform verifies binaries, not the spec document that defines it.",
"category": "functional",
"criticality": "low",
"verification": "test",
"asil": "QM"
}
]
}
1 change: 1 addition & 0 deletions evidence.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
//fusa:req REQ-RELAY-092
//fusa:req REQ-RELAY-093
//fusa:req REQ-RELAY-094
//fusa:req REQ-RELAY-100
//fusa:req REQ-RELAY-074
//fusa:req REQ-RELAY-075
//fusa:req REQ-RELAY-076
Expand Down
36 changes: 36 additions & 0 deletions spec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# RELAY Spec Changelog

## v2.7.1 — 2026-08-21 (doc addition; no normative change to existing conformant implementations)

- **New §19.5 "This document's own version literals".** Requirement 14
(§17) already obligates an *implementation's* declared `spec_version` to
trace back to `spec/version.json` rather than a hand-copied literal — but
this document was never held to the same standard for its own text. The
stale `"0.1"` example literals fixed by hand in v2.2.3 were exactly that
class of drift: §12.1/§12.2's JSON examples and §13.5's Docker `LABEL`
example were three major versions behind current, in a document telling
implementers not to let exactly this happen. §19.5 makes it a normative
MUST that every version literal in this document's own text — the
`"spec_version"` examples (§12.1, §12.2, §12.4, §17.2, §20.6), the
`LABEL io.relay.spec-version` example (§13.5), and §19.4's "Current
version" line and Go/C++/Rust snippets — matches `spec/version.json`,
and that this repository's own CI enforces it.
- **Reference implementation**: `TestSpecVersionMatchesVersionJSON` and
`TestSpecDocumentVersionLiteralsMatchVersionJSON`
(`version_consistency_test.go`) read `spec/version.json` and
`spec/relay-spec.md` from the package's own embedded evidence
(`evidence.go`) and fail if any of the anchors above, or any
`"spec_version": "X.Y"` example literal anywhere in the document, has
drifted. New `REQ-RELAY-100`. Mutation-tested: bumped
`spec/version.json` to a throwaway value and confirmed both tests failed
with a precise diff for every one of the 12 literals checked (5 named
anchors + 7 example occurrences), then separately reworded one anchor's
surrounding prose and confirmed the test reports "pattern not found"
rather than silently passing; both restored and re-verified green.
- **Not a `relay conform` check.** `relay conform` verifies binaries, not
the specification document that defines it — this sits alongside
Requirements 13/14 (§17) as a repository-CI-verified discipline, not a
new black-box conformance requirement on implementations. No new §17
Requirement, no change to implementation-facing obligations.
- `SpecVersion` unchanged (`2.7`); the only thing enforced is that it
and its document-text copies stay in lockstep going forward. Closes
[NEW-SPEC-6].

## v2.7 — 2026-08-21 (MINOR — new protocol/model retirement process, §17 Requirement 17)

- **New §3.2 "Retiring a protocol or model".** §19.2's existing deprecation
Expand Down
30 changes: 28 additions & 2 deletions spec/relay-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1455,8 +1455,10 @@ LABEL io.relay.spec-version="2.7"
```

The `io.relay.spec-version` label MUST always match the value of `SpecVersion`
exported by the package (§17.12 / §19.4). The `"2.7"` shown above is an example;
update it on each spec minor release.
exported by the package (§17.12 / §19.4). The `"2.7"` shown above is an
example; per §19.5, this document's own copy of it is checked by this
repository's CI against `spec/version.json` on every commit, the same way
Requirement 14 requires of a conformant implementation.

The project directory is mounted at `/project` by convention:

Expand Down Expand Up @@ -3185,6 +3187,30 @@ Current version: **v2.7**
**C++:** `constexpr std::string_view kRelaySpecVersion = "2.7";`
**Rust:** `pub const RELAY_SPEC_VERSION: &str = "2.7";`

### 19.5 This document's own version literals

Requirement 14 (§17) obligates an *implementation's* declared `spec_version`
to trace back to `spec/version.json` rather than a hand-copied literal kept
in sync by memory. This document is not exempt from the same discipline:
every version literal appearing in this specification's own text — the
`"spec_version"` value in each JSON example (§12.1, §12.2, §12.4, §17.2,
§20.6), the `LABEL io.relay.spec-version` value in §13.5's Docker example,
and this section's own "Current version" line and Go/C++/Rust snippets
above — MUST match `spec/version.json` at every commit. A stale example is
not merely cosmetic: it is this document teaching implementers the exact
hand-copied-literal drift Requirement 14 exists to prevent.

This repository's own CI MUST fail the build when any of the above diverges
from `spec/version.json`, the same way Requirement 14 requires of a
conformant implementation's CI. This is a discipline on the RELAY
specification repository itself, not a new `relay conform` check: `relay
conform` verifies binaries, not the spec document that defines it, so this
sits alongside Requirements 13/14 (§17) as verified by the repository's own
CI/test suite rather than by `relay conform`'s black-box interface. (The
`spec/vectors/` distribution's own `vectors_version` illustration in §15.8
is a deliberate exception: it demonstrates the vector-pinning mechanism
itself and is not required to track the current spec version — see §15.8.)

---

## 20. Continuous Conformance
Expand Down
106 changes: 106 additions & 0 deletions version_consistency_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package relay

import (
"encoding/json"
"regexp"
"testing"
)

// versionJSONVersion reads spec/version.json's authoritative "version"
// field — the single source of truth per §19.4.
func versionJSONVersion(t *testing.T) string {
t.Helper()
raw, err := Evidence("version")
if err != nil {
t.Fatalf("Evidence(version): %v", err)
}
var meta struct {
Version string `json:"version"`
}
if err := json.Unmarshal(raw, &meta); err != nil {
t.Fatalf("unmarshal spec/version.json: %v", err)
}
if meta.Version == "" {
t.Fatal("spec/version.json: version field is empty")
}
return meta.Version
}

// TestSpecVersionMatchesVersionJSON is the mechanism behind §19.4/§19.5:
// SpecVersion (version.go) is a hand-copied literal by construction — Go
// constants can't be computed from an embedded file at compile time — but
// this test makes any drift from spec/version.json a CI failure instead of
// a silent, forgotten bump.
//
//fusa:test REQ-RELAY-100
func TestSpecVersionMatchesVersionJSON(t *testing.T) {
want := versionJSONVersion(t)
if SpecVersion != want {
t.Errorf("SpecVersion = %q, spec/version.json version = %q — one was bumped without the other", SpecVersion, want)
}
}

// TestSpecDocumentVersionLiteralsMatchVersionJSON enforces §19.5: every
// canonical version literal in the specification document's own text —
// not just what implementations print, which Requirement 14 already
// governs — MUST match spec/version.json. The stale "0.1" example
// literals fixed by hand in v2.2.3 are exactly the class of drift this
// closes; this makes it a CI failure instead of relying on the next
// person to notice.
//
//fusa:test REQ-RELAY-100
func TestSpecDocumentVersionLiteralsMatchVersionJSON(t *testing.T) {
want := versionJSONVersion(t)

specRaw, err := Evidence("specification")
if err != nil {
t.Fatalf("Evidence(specification): %v", err)
}
spec := string(specRaw)

// Each of these anchors is expected to appear exactly once in
// §13.5/§19.4/§19.5; a missing match means the surrounding prose was
// reworded and this regex needs to move with it, not that the check
// should be silently skipped.
anchors := []struct {
name string
re *regexp.Regexp
}{
{`§19.4 "Current version" line`, regexp.MustCompile(`Current version: \*\*v([0-9]+\.[0-9]+)\*\*`)},
{"§19.4 Go snippet", regexp.MustCompile(`const SpecVersion = "([0-9]+\.[0-9]+)"`)},
{"§19.4 C++ snippet", regexp.MustCompile(`kRelaySpecVersion = "([0-9]+\.[0-9]+)"`)},
{"§19.4 Rust snippet", regexp.MustCompile(`RELAY_SPEC_VERSION: &str = "([0-9]+\.[0-9]+)"`)},
{"§13.5 Docker LABEL", regexp.MustCompile(`LABEL io\.relay\.spec-version="([0-9]+\.[0-9]+)"`)},
}
for _, a := range anchors {
m := a.re.FindStringSubmatch(spec)
if m == nil {
t.Errorf("%s: pattern not found in spec/relay-spec.md — reworded? update this test's regex", a.name)
continue
}
if m[1] != want {
t.Errorf("%s: found %q, want %q (spec/version.json)", a.name, m[1], want)
}
}

// Every `"spec_version": "X.Y"` example literal across §12.1, §12.2,
// §12.4, §17.2's manifest example, §20.6's attestation example, and
// any future one MUST also match. Deliberately excludes §19.3's
// `"<targeted-version>"` placeholder (not a numeric literal) and
// §15.8's `"vectors_version"` illustration (a different field,
// intentionally pinned independently — see §15.8/§19.5).
exampleRe := regexp.MustCompile(`"spec_version":\s*"([0-9]+\.[0-9]+)"`)
matches := exampleRe.FindAllStringSubmatch(spec, -1)
if len(matches) == 0 {
t.Fatal(`no "spec_version": "X.Y" example literal found in spec/relay-spec.md — has the example format changed? update this test`)
}
for _, m := range matches {
if m[1] != want {
t.Errorf(`"spec_version": %q example literal found in spec/relay-spec.md, want %q`, m[1], want)
}
}
}
Loading