diff --git a/.fusa-reqs.json b/.fusa-reqs.json index c94c7f3..a3491a8 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -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" } ] } diff --git a/evidence.go b/evidence.go index 1af2844..cc66d81 100644 --- a/evidence.go +++ b/evidence.go @@ -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 diff --git a/spec/CHANGELOG.md b/spec/CHANGELOG.md index 110a299..78cdebf 100644 --- a/spec/CHANGELOG.md +++ b/spec/CHANGELOG.md @@ -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 diff --git a/spec/relay-spec.md b/spec/relay-spec.md index 101a6d0..bb96059 100644 --- a/spec/relay-spec.md +++ b/spec/relay-spec.md @@ -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: @@ -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 diff --git a/version_consistency_test.go b/version_consistency_test.go new file mode 100644 index 0000000..1d2ecc4 --- /dev/null +++ b/version_consistency_test.go @@ -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 + // `""` 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) + } + } +}