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
10 changes: 10 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,16 @@
"verification": "test",
"safety_goal": "SG-006",
"asil": "ASIL-B"
},
{
"id": "REQ-RELAY-096",
"title": "Vector distribution manifest (relay-vectors/1)",
"text": "spec/vectors/vectors_manifest.json (spec \u00a715.8) MUST pin the canonical golden vector distribution: a vectors_version bound to spec/version.json, and a SHA-256 (lowercase hex) per file under spec/vectors/ (including spec/vectors/errors/). VectorNames() MUST exclude the manifest itself from the vector set it enumerates \u2014 it is metadata about the distribution, not a vector. VerifyVectorManifest() MUST recompute every embedded vector's SHA-256 and report a finding for any vector present but unlisted, listed but absent, or present-and-listed with a mismatched hash (spec \u00a717 Requirement 16).",
"category": "functional",
"criticality": "medium",
"verification": "test",
"safety_goal": "SG-006",
"asil": "ASIL-B"
}
]
}
5 changes: 5 additions & 0 deletions cmd/relay/jsonschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ func TestGoldenVectorsConformToSchemas(t *testing.T) {
}

for _, p := range paths {
// spec/vectors/vectors_manifest.json (§15.8) pins the vector
// distribution; it is metadata about the set, not a vector itself.
if filepath.Base(p) == "vectors_manifest.json" {
continue
}
p := p
t.Run(filepath.Base(p), func(t *testing.T) {
data, err := os.ReadFile(p)
Expand Down
2 changes: 1 addition & 1 deletion cmd/relay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
relay "github.com/SoundMatt/RELAY/v2"
)

const toolVersion = "2.3.0"
const toolVersion = "2.4.0"

func main() {
if err := run(os.Stdout, os.Stderr, os.Args[1:]); err != nil {
Expand Down
37 changes: 37 additions & 0 deletions spec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
# RELAY Spec Changelog

## v2.4 — 2026-08-21 (MINOR — new §17 conformance requirement)

- **New §17 Requirement 16 — Vector manifest.** The canonical
`spec/vectors/` distribution (new §15.8) is pinned by
`spec/vectors/vectors_manifest.json`. A conformant implementation that
embeds a local copy of these vectors MUST embed the exact pinned set,
and MUST have a CI step that fails when its embedded copy's SHA-256
diverges from the published manifest for the `vectors_version` it
targets. Same MINOR-worthy precedent as Requirements 13, 14, and 15.
- **New §15.8 — Vector distribution.** Defines the `relay-vectors/1`
manifest shape (`kind`, `manifest_version`, `vectors_version`,
`vectors[]` — each a `{name, sha256}` pair). New embedded schema
`spec/schemas/vectors-manifest.json`. Explicitly scoped to
*distribution* of the existing envelope-level `relay.Message` fixtures
only: per §1.1/§17.1, wire format remains out of scope, and this
manifest MUST NOT be extended with byte-for-byte wire-format vectors.
(NEW-SPEC-1's originating issue proposed extending the vector set
itself to wire-format bug classes; that part is explicitly declined as
contradicting RELAY's own pre-existing scope boundary, established in
REL-SPEC-2/§17.1 — only the manifest/pinning/hashing mechanism ships
here.)
- **New §20.1 CI gate**: recompute the SHA-256 of every embedded golden
vector and fail the job on any divergence from
`spec/vectors/vectors_manifest.json` for the `vectors_version` it
targets.
- **Reference implementation**: `spec/vectors/vectors_manifest.json`
(16 vectors, hashes independently verified via `sha256sum`),
`VectorsManifest()`/`ParsedVectorsManifest()`/`VerifyVectorManifest()` in
this repo's own `vectors.go`. `VectorNames()` now excludes the manifest
file itself from the vector set it enumerates (it is metadata about the
distribution, not a vector) — this also required a matching fix in two
pre-existing test files (`spec_vectors_test.go`,
`cmd/relay/jsonschema_test.go`) that globbed `spec/vectors/*.json`
directly and would otherwise have tried to parse the manifest as a
golden vector. New `REQ-RELAY-096`. `SpecVersion` bumped to `2.4`.
Closes [NEW-SPEC-2].

## v2.3 — 2026-08-21 (MINOR — new §17 conformance requirement)

- **New §17 Requirement 15 — Conformance manifest.** The implementation
Expand Down
89 changes: 75 additions & 14 deletions spec/relay-spec.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# RELAY Specification — v2.3
# RELAY Specification — v2.4

**Real-time Embedded Link Abstraction Yoke**

Expand Down Expand Up @@ -1182,7 +1182,7 @@ failure rather than a skip. Exit: `0` all equivalent, `1` any mismatch/error,
"protocol": "CAN",
"protocol_int": 1,
"version": "1.2.3",
"spec_version": "2.3",
"spec_version": "2.4",
"language": "go",
"runtime": "go1.25.0",
"commit": "a1b2c3d4"
Expand All @@ -1207,7 +1207,7 @@ conformance failure, but §17.2's conformance manifest cannot populate its own
"protocol": "CAN",
"protocol_int": 1,
"version": "1.2.3",
"spec_version": "2.3",
"spec_version": "2.4",
"commands": ["version", "capabilities", "status", "connect", "send", "subscribe"],
"transports": ["socketcan", "virtual"],
"features": ["fd", "isotp", "j1939"],
Expand Down Expand Up @@ -1278,7 +1278,7 @@ $ go-can version --format json
"protocol": "CAN",
"protocol_int": 1,
"version": "1.2.3",
"spec_version": "2.3",
"spec_version": "2.4",
"language": "go",
"runtime": "go1.25.0"
}
Expand All @@ -1290,7 +1290,7 @@ $ go-can capabilities
"protocol": "CAN",
"protocol_int": 1,
"version": "1.2.3",
"spec_version": "2.3",
"spec_version": "2.4",
"commands": ["version", "capabilities", "status", "connect", "send", "subscribe"],
"transports": ["socketcan", "virtual"],
"features": ["fd", "isotp", "j1939"],
Expand Down Expand Up @@ -1366,11 +1366,11 @@ LABEL org.opencontainers.image.licenses="MPL-2.0"
LABEL io.relay.tool="<tool>"
LABEL io.relay.language="go|cpp|rust|c"
LABEL io.relay.binary="<binary>"
LABEL io.relay.spec-version="2.3"
LABEL io.relay.spec-version="2.4"
```

The `io.relay.spec-version` label MUST always match the value of `SpecVersion`
exported by the package (§17.12 / §19.4). The `"2.3"` shown above is an example;
exported by the package (§17.12 / §19.4). The `"2.4"` shown above is an example;
update it on each spec minor release.

The project directory is mounted at `/project` by convention:
Expand Down Expand Up @@ -2123,6 +2123,54 @@ The conversion MUST be lossless (§15.7): every SOME/IP header field is preserve

`FromMessage`: parse `msg.ID` as `"serviceID/methodID"` decimal pair; if malformed return `ErrMalformedMessage`.

### 15.8 Vector distribution

The canonical golden vectors (§15.7, `spec/vectors/*.json`) are a single
source of truth: every implementation that participates in `relay interop`
or Requirement 9's envelope-conversion verification embeds an identical
copy, not a private variant. `spec/vectors/vectors_manifest.json` pins that
distribution:

```json
{
"kind": "relay-vectors-manifest",
"manifest_version": "relay-vectors/1",
"vectors_version": "2.4",
"vectors": [
{"name": "can-standard-frame", "sha256": "816b402d..."},
{"name": "errors/lin-diagnostic-wrong-checksum", "sha256": "36666ad1..."}
]
}
```

`vectors_version` is bound to the RELAY spec version (§19.4) that last
changed the vector set — not necessarily every spec release, since most
releases don't touch `spec/vectors/`. `vectors` MUST list every file under
`spec/vectors/` (including `spec/vectors/errors/`), by name relative to
that directory with the `.json` suffix stripped, each paired with the
SHA-256 (lowercase hex) of its raw file bytes.

Per Requirement 16 (§17), a conformant implementation that embeds a local
copy of these vectors (as RELAY's own reference tooling does, via
`//go:embed` in `vectors.go`) MUST embed the *exact* pinned set —
byte-for-byte — and MUST have a CI step that recomputes each embedded
vector's SHA-256 and fails the build on any divergence from
`vectors_manifest.json` for the `vectors_version` it targets. This closes
the same class of drift Requirement 15's conformance manifest closes for
§17 itself: a golden vector silently diverging from the canonical set (an
accidental local edit, a stale copy after an upstream fix) would otherwise
pass every existing check, since neither `relay conform` nor `relay
interop` currently distinguishes "this implementation's copy of a vector"
from "the canonical vector."

`spec/vectors/*.json` content itself is unaffected: this section pins
*distribution*, not envelope semantics (already governed by §15.7). Wire
format remains explicitly out of scope per §1.1/§17.1: this manifest MUST
NOT be extended with byte-for-byte wire-format vectors, only the existing
envelope-level `relay.Message` fixtures. §17.1's SHOULD for
implementation-owned wire-format regression vectors is unaffected by this
section and remains a separate, unpinned concern.

---

## 16. Per-Protocol Defaults
Expand Down Expand Up @@ -2162,6 +2210,7 @@ An implementation is **RELAY-conformant** if and only if:
13. **Capabilities generation.** The capabilities document's `commands`, `transports`, `features`, `interfaces`, and `optional_interfaces` fields are generated from the same source-of-truth that gates compilation, per §12.2, not hand-maintained independently of it.
14. **Spec-version binding.** The `spec_version` value the CLI prints (§12.1, §12.2) MUST NOT be a hand-copied literal kept in sync by memory alone. A Go implementation that depends on `github.com/SoundMatt/RELAY` MUST bind its declared value directly to `relay.SpecVersion` (e.g. `const SpecVersion = relay.SpecVersion`), so a dependency bump alone keeps it current. An implementation in any other language, or a Go implementation that does not depend on the RELAY module, MUST have a CI step that fails the build when its declared `spec_version` diverges from the authoritative `spec/version.json` (§19.4) at the RELAY revision it targets.
15. **Conformance manifest.** The implementation MUST commit a `relay-conform/1` manifest (§17.2), generated by `relay conform --manifest` against its own built binary; CI MUST regenerate the manifest on every change and fail the build on any diff from the committed copy, or on any requirement entry whose `status` is `FAIL`.
16. **Vector manifest.** The canonical `spec/vectors/` distribution (§15.8) is pinned by `spec/vectors/vectors_manifest.json`. A conformant implementation that embeds a local copy of these vectors MUST embed the exact pinned set, and MUST have a CI step that fails when its embedded copy's SHA-256 diverges from the published manifest for the `vectors_version` it targets.

`relay conform <binary>` is a **black-box CLI tool**: it can only observe what
the built binary's `version`/`capabilities`/`status` commands print, not the
Expand Down Expand Up @@ -2205,7 +2254,14 @@ manifest) is split down the middle: `relay conform --manifest` itself
conform`'s own reach, unlike Requirements 2–5/8–11/13/14 above — but
whether CI actually regenerates it on every change and diffs it against
the committed copy is, like Requirement 14, a CI-process fact `relay
conform` cannot observe from outside the binary it's given.
conform` cannot observe from outside the binary it's given. Requirement 16
(vector manifest) is the same category as Requirement 14, not Requirement
15: unlike the conformance manifest, `relay conform` has no CLI surface
that exposes an implementation's embedded vector bytes at all, so it
cannot even partially generate this check — whether an implementation's
embedded vector copy matches the published manifest is entirely a
CI-process fact about the implementation's own build, verified by the
implementation's own CI instead.

**Requirement-to-verifier lookup table**, collecting the narrative above
into one place:
Expand All @@ -2227,6 +2283,7 @@ into one place:
| 13 | Capabilities generation | Implementation's own build process and test suite | Not observable through the CLI |
| 14 | Spec-version binding | Implementation's own CI | Not observable through the CLI |
| 15 | Conformance manifest | `relay conform --manifest` (content); implementation's own CI (regenerate + diff) | Split — manifest content generated by `relay conform`; CI enforcement not observable through the CLI |
| 16 | Vector manifest | Implementation's own CI | Not observable through the CLI |

### 17.1 Wire-format regression coverage (recommended)

Expand Down Expand Up @@ -2280,7 +2337,7 @@ diffable artifact.
"manifest_version": "relay-conform/1",
"tool": "go-can",
"binary_version": "1.2.3",
"spec_version": "2.3",
"spec_version": "2.4",
"git_sha": "a1b2c3d4",
"capabilities_sha256": "e3b0c44298fc1c14...",
"requirements": [
Expand All @@ -2299,7 +2356,7 @@ SHA-256 of the raw `capabilities` document bytes as returned by the binary,
letting a diff catch any change to the capabilities document — mechanically
regenerated or not — without re-parsing it.

`requirements` MUST contain exactly one entry per §17 requirement (1–15,
`requirements` MUST contain exactly one entry per §17 requirement (1–16,
extended as this document's own requirement list grows). Each entry's
`status` is one of:

Expand Down Expand Up @@ -3029,11 +3086,11 @@ clarifications and fixes in PATCH releases.

`spec/version.json` is authoritative. The spec document title is informational.

Current version: **v2.3**
Current version: **v2.4**

**Go:** `const SpecVersion = "2.3"` (update in implementations targeting v2.3)
**C++:** `constexpr std::string_view kRelaySpecVersion = "2.3";`
**Rust:** `pub const RELAY_SPEC_VERSION: &str = "2.3";`
**Go:** `const SpecVersion = "2.4"` (update in implementations targeting v2.4)
**C++:** `constexpr std::string_view kRelaySpecVersion = "2.4";`
**Rust:** `pub const RELAY_SPEC_VERSION: &str = "2.4";`

---

Expand Down Expand Up @@ -3064,6 +3121,10 @@ every pull request, run and gate on:
`relay-conform/1` manifest with `relay conform --manifest` against the
built CLI and fail the job on any diff from the committed copy, or on
any requirement entry whose `status` is `FAIL`.
5. **Vector manifest** (§15.8, Requirement 16) — recompute the SHA-256 of
every embedded golden vector and fail the job on any divergence from
`spec/vectors/vectors_manifest.json` for the `vectors_version` it
targets.

Releases MUST be tagged only from a commit whose CI is fully green.

Expand Down
43 changes: 43 additions & 0 deletions spec/schemas/vectors-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/SoundMatt/RELAY/spec/schemas/vectors-manifest.json",
"title": "RELAY golden vector distribution manifest",
"description": "spec/vectors/vectors_manifest.json — pins the canonical golden vector distribution (spec §15.8, Requirement 16).",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "relay-vectors-manifest"
},
"manifest_version": {
"type": "string",
"const": "relay-vectors/1"
},
"vectors_version": {
"type": "string",
"description": "The RELAY spec version (spec/version.json) that last changed the vector set."
},
"vectors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Vector name relative to spec/vectors/, .json suffix stripped, e.g. \"can-standard-frame\" or \"errors/lin-diagnostic-wrong-checksum\"."
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$",
"description": "SHA-256 (lowercase hex) of the vector file's raw bytes."
}
},
"required": ["name", "sha256"],
"additionalProperties": false
},
"description": "One entry per file under spec/vectors/ (including spec/vectors/errors/)."
}
},
"required": ["kind", "manifest_version", "vectors_version", "vectors"],
"additionalProperties": false
}
23 changes: 23 additions & 0 deletions spec/vectors/vectors_manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"kind": "relay-vectors-manifest",
"manifest_version": "relay-vectors/1",
"vectors_version": "2.4",
"vectors": [
{"name": "can-fd-extended-frame", "sha256": "b4c84bb35ec2c07463a6d4d900cd8789a94e564a8f4897eb3be500ba3f3c0247"},
{"name": "can-standard-frame", "sha256": "816b402dca5070ec011863b8d8eb3b97abbf2ea8cf264706113e06fcceb4fe27"},
{"name": "can-xl-frame", "sha256": "0b3e3e80aa239c16ce6d745fbb01ab3fb30307c5328af29647c44d602d425ad1"},
{"name": "dds-sample", "sha256": "d2ec5e026defa1eca904ee7cb3db8e71d49edd5d0b9123b2a7d05ecc2478bf1f"},
{"name": "errors/can-fd-xl-mutually-exclusive", "sha256": "f0322acd9ba36c5d368326537946677632ddbb7c3bfdb4f9620c6bd53145268d"},
{"name": "errors/can-rtr-with-fd", "sha256": "9eac49e7dc6742bf8a33be86e14c011f30889776f41b510904bfa003d319f850"},
{"name": "errors/can-standard-id-overflow", "sha256": "c4b1d8c3ce9c88e19cbbef09f21d50c5ae3cd699a5ebbc6122eb0ab9bac17f28"},
{"name": "errors/can-xl-priority-id-overflow", "sha256": "e6dcbefebc53ed90dae6195c2870d8bce40099f15910388bcf37141cea059328"},
{"name": "errors/dds-domain-out-of-range", "sha256": "3fcebeccd26d4f013d2d11cc224d5a95cf423007287cf92d8b09413ef253978f"},
{"name": "errors/lin-diagnostic-wrong-checksum", "sha256": "36666ad1c49a89ce2b8fbc34475bfe3d5482d232b7b4b315ccb6248b7a05f776"},
{"name": "errors/lin-id-overflow", "sha256": "26ef330bab6af500167b26d24cafabe6444b64162518503577fd3702cc85780f"},
{"name": "errors/someip-wrong-protocol-version", "sha256": "ceac6a36bba85455afdf8c95481c1dec59c022f43cfd9347e229ac88de80117e"},
{"name": "lin-frame", "sha256": "4b17ddbf4f12b909acd1e964bcc9c4e99ab3bd396c29344bdc4e6c9d287db62b"},
{"name": "mqtt-message", "sha256": "8cda632307125bcb60b2b9a7ead020ddab4ef521cfa11703511453e204728263"},
{"name": "rcp-message", "sha256": "9cc8030a4035cdf9807fa726a2f56c05ad6e6b8807b335c166c9fc780f36d7a5"},
{"name": "someip-message", "sha256": "89b7ca3edb8e233183d04f8870607edd40e5653b9e68e438da63d2aa866c8432"}
]
}
4 changes: 2 additions & 2 deletions spec/version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "2.3",
"date": "2026-08-20",
"version": "2.4",
"date": "2026-08-21",
"status": "stable",
"networks": [
"CAN",
Expand Down
5 changes: 5 additions & 0 deletions spec_vectors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ func TestGoldenVectorsRoundTrip(t *testing.T) {
}

for _, p := range paths {
// spec/vectors/vectors_manifest.json (§15.8) pins the vector
// distribution; it is metadata about the set, not a vector itself.
if filepath.Base(p) == "vectors_manifest.json" {
continue
}
p := p
t.Run(filepath.Base(p), func(t *testing.T) {
data, err := os.ReadFile(p)
Expand Down
Loading
Loading