From 6b6612396b9eb70cb044da78e6086ec08c2d7f1e Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:09:34 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20relay=20conform=20--manifest?= =?UTF-8?q?=20(relay-conform/1),=20spec=20=C2=A717.2,=20Requirement=2015?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New §17 Requirement 15: implementations MUST commit a relay-conform/1 manifest generated by `relay conform --manifest`; CI MUST regenerate and fail the build on any diff or any FAIL requirement entry. - New §17.2 defines the manifest JSON shape and a 4-value status vocabulary (PASS/FAIL/SHAPE_ONLY/NOT_OBSERVABLE), deliberately distinct from relay conform's existing PASS/WARN/FAIL finding severities: a manifest MUST NOT report PASS for a requirement relay conform cannot actually observe. - New optional `commit` field on the §12.1 version document (backward compatible) so a manifest can populate its own git_sha. - New §20.1 CI gate for manifest regeneration/diffing. - New embedded schema spec/schemas/relay-conform-manifest.json. - Reference implementation: --manifest flag + buildManifest() in cmd/relay/conform.go. Only Requirements 1, 6, 7, 12 get real PASS/FAIL/SHAPE_ONLY statuses (mirroring what relay conform can actually observe black-box); everything else, including Requirement 15's own entry, is honestly NOT_OBSERVABLE. - New REQ-RELAY-095 (not a reuse of REQ-RELAY-052 — this is materially new behavior: new output format, new status vocabulary, new artifact). 7 new tests cover schema validity, capabilities_sha256 correctness, NOT_OBSERVABLE defaults, FAIL propagation, and CLI exit codes. - SpecVersion bumped 2.2 -> 2.3 (MINOR, matching the Requirement 13/14 precedent: a new numbered §17 requirement is MINOR-worthy even where relay conform cannot fully observe it). Closes #139. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 10 ++ cmd/relay/conform.go | 170 ++++++++++++++++++++- cmd/relay/conform_test.go | 179 +++++++++++++++++++++++ cmd/relay/main.go | 2 +- spec/CHANGELOG.md | 35 +++++ spec/relay-spec.md | 105 +++++++++++-- spec/schemas/cli-version.json | 4 + spec/schemas/relay-conform-manifest.json | 69 +++++++++ spec/version.json | 2 +- version.go | 2 +- version_test.go | 4 +- 11 files changed, 562 insertions(+), 20 deletions(-) create mode 100644 spec/schemas/relay-conform-manifest.json diff --git a/.fusa-reqs.json b/.fusa-reqs.json index b83403e..2db15ca 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -892,6 +892,16 @@ "criticality": "medium", "verification": "spec-review", "asil": "QM" + }, + { + "id": "REQ-RELAY-095", + "title": "relay conform --manifest (relay-conform/1)", + "text": "relay conform --manifest MUST emit a relay-conform/1 manifest (spec \u00a717.2) instead of findings: a JSON document with kind, manifest_version, tool, binary_version, spec_version, git_sha, capabilities_sha256, a requirements[] entry for every \u00a717 requirement (id/status/verifier), and an overall PASS/FAIL. status MUST use the vocabulary PASS/FAIL/SHAPE_ONLY/NOT_OBSERVABLE and MUST NOT report PASS for a requirement relay conform cannot actually observe. The process MUST exit 1 when overall is FAIL.", + "category": "functional", + "criticality": "high", + "verification": "test", + "safety_goal": "SG-006", + "asil": "ASIL-B" } ] } diff --git a/cmd/relay/conform.go b/cmd/relay/conform.go index e8679f6..bf0c46f 100644 --- a/cmd/relay/conform.go +++ b/cmd/relay/conform.go @@ -7,6 +7,8 @@ package main import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "flag" "fmt" @@ -53,19 +55,22 @@ type conformResult struct { Findings []conformFinding `json:"findings"` } -// runConform implements `relay conform [--format text|json] [--strict] `. +// runConform implements +// `relay conform [--format text|json] [--strict] [--manifest] `. // //fusa:req REQ-RELAY-052 +//fusa:req REQ-RELAY-095 func runConform(stdout, stderr io.Writer, args []string) error { fs := flag.NewFlagSet("conform", flag.ContinueOnError) fs.SetOutput(io.Discard) format := fs.String("format", "text", "Output format: text or json") strict := fs.Bool("strict", false, "Treat WARN as FAIL") + manifest := fs.Bool("manifest", false, "Emit a relay-conform/1 manifest (spec §17.2) instead of findings") if err := fs.Parse(args); err != nil { return fmt.Errorf("relay conform: %w", err) } if fs.NArg() == 0 { - fmt.Fprintln(stderr, "Usage: relay conform [--format text|json] [--strict] ") + fmt.Fprintln(stderr, "Usage: relay conform [--format text|json] [--strict] [--manifest] ") return exitCode(2) } if fs.NArg() > 1 { @@ -78,6 +83,19 @@ func runConform(stdout, stderr io.Writer, args []string) error { return exitCode(2) } + if *manifest { + m := buildManifest(fs.Arg(0)) + enc := json.NewEncoder(stdout) + enc.SetIndent("", " ") + if err := enc.Encode(m); err != nil { + return err + } + if m.Overall == statusFail { + return exitCode(1) + } + return nil + } + cr := conformBinary(fs.Arg(0), *strict) switch *format { @@ -152,6 +170,154 @@ func conformBinary(binary string, strict bool) conformResult { return conformResult{Binary: binary, Result: result, Findings: all} } +// manifestStatus is a per-requirement status in a relay-conform/1 manifest +// (spec §17.2). It is deliberately a narrower vocabulary than conformSeverity: +// a manifest never reports PASS for a requirement relay conform cannot +// actually observe. +type manifestStatus string + +const ( + statusPass manifestStatus = "PASS" + statusFail manifestStatus = "FAIL" + statusShapeOnly manifestStatus = "SHAPE_ONLY" + statusNotObservable manifestStatus = "NOT_OBSERVABLE" + verifierRelayConform = "relay conform" + verifierTestSuite = "implementation test suite" + verifierBuildAndTest = "implementation's own build process and test suite" + verifierCI = "implementation's own CI" + verifierManifestSplit = "relay conform (manifest content); implementation's own CI (regenerate + diff)" +) + +// manifestRequirement is one entry in a relay-conform/1 manifest's +// "requirements" array (spec §17.2). +type manifestRequirement struct { + ID int `json:"id"` + Status manifestStatus `json:"status"` + Verifier string `json:"verifier"` +} + +// conformManifest is the relay-conform/1 manifest document (spec §17.2, +// §17 Requirement 15). +type conformManifest struct { + Kind string `json:"kind"` + ManifestVersion string `json:"manifest_version"` + Tool string `json:"tool"` + BinaryVersion string `json:"binary_version"` + SpecVersion string `json:"spec_version"` + GitSHA string `json:"git_sha"` + CapabilitiesSHA256 string `json:"capabilities_sha256"` + Requirements []manifestRequirement `json:"requirements"` + Overall manifestStatus `json:"overall"` +} + +// buildManifest generates a relay-conform/1 manifest (spec §17.2) for one +// binary. It fetches version/capabilities/status exactly once each — the +// same single-fetch discipline as conformBinary — so a manifest and a +// findings report generated from the same invocation never see different +// binary output. +// +//fusa:req REQ-RELAY-095 +func buildManifest(binary string) conformManifest { + m := conformManifest{ + Kind: "relay-conform-manifest", + ManifestVersion: "relay-conform/1", + } + + versionJSON, verErr := runBinaryCommand(binary, []string{"version", "--format", "json"}) + var vFindings []conformFinding + if verErr != nil { + vFindings = []conformFinding{fail("§17.7", "version --format json failed: %v", verErr)} + } else { + vFindings = validateVersionDoc(versionJSON) + var vdoc struct { + Tool string `json:"tool"` + Version string `json:"version"` + SpecVersion string `json:"spec_version"` + Commit string `json:"commit"` + } + _ = json.Unmarshal(versionJSON, &vdoc) + m.Tool = vdoc.Tool + m.BinaryVersion = vdoc.Version + m.SpecVersion = vdoc.SpecVersion + m.GitSHA = vdoc.Commit + } + + capsJSON, capsErr := runBinaryCommand(binary, []string{"capabilities"}) + var cFindings []conformFinding + if capsErr != nil { + cFindings = []conformFinding{fail("§17.7", "capabilities failed: %v", capsErr)} + } else { + cFindings = validateCapabilitiesDoc(capsJSON) + sum := sha256.Sum256(capsJSON) + m.CapabilitiesSHA256 = hex.EncodeToString(sum[:]) + } + + statusJSON, statusErr := runBinaryCommand(binary, []string{"status", "--format", "json"}) + if statusErr != nil { + statusJSON, statusErr = runBinaryCommand(binary, []string{"status"}) + } + var sFindings []conformFinding + if statusErr != nil { + sFindings = []conformFinding{fail("§17.7", "status command failed: %v", statusErr)} + } else { + sFindings = validateStatusDoc(statusJSON) + } + + req1 := statusPass + if hasFail(vFindings) { + req1 = statusFail + } + req6 := statusPass + if hasFail(cFindings) { + req6 = statusFail + } + req7 := statusPass + if hasFail(vFindings) || hasFail(cFindings) || hasFail(sFindings) { + req7 = statusFail + } + req12 := statusShapeOnly + if hasFail(vFindings) { + req12 = statusFail + } + + m.Requirements = []manifestRequirement{ + {1, req1, verifierRelayConform}, + {2, statusNotObservable, verifierTestSuite}, + {3, statusNotObservable, verifierTestSuite}, + {4, statusNotObservable, verifierTestSuite}, + {5, statusNotObservable, verifierTestSuite}, + {6, req6, verifierRelayConform}, + {7, req7, verifierRelayConform}, + {8, statusNotObservable, verifierTestSuite}, + {9, statusNotObservable, verifierTestSuite}, + {10, statusNotObservable, verifierTestSuite}, + {11, statusNotObservable, verifierTestSuite}, + {12, req12, verifierRelayConform}, + {13, statusNotObservable, verifierBuildAndTest}, + {14, statusNotObservable, verifierCI}, + {15, statusNotObservable, verifierManifestSplit}, + } + + m.Overall = statusPass + for _, r := range m.Requirements { + if r.Status == statusFail { + m.Overall = statusFail + break + } + } + return m +} + +// hasFail reports whether any finding in fs is FAIL-severity. +func hasFail(fs []conformFinding) bool { + for _, f := range fs { + if f.Severity == sevFail { + return true + } + } + return false +} + func printConformText(w io.Writer, cr conformResult) { for _, f := range cr.Findings { fmt.Fprintf(w, "%-4s %s %s\n", f.Severity, f.Req, f.Message) diff --git a/cmd/relay/conform_test.go b/cmd/relay/conform_test.go index ddebbdb..bcf223a 100644 --- a/cmd/relay/conform_test.go +++ b/cmd/relay/conform_test.go @@ -6,6 +6,8 @@ package main import ( "bytes" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "os" @@ -13,6 +15,8 @@ import ( "path/filepath" "strings" "testing" + + relay "github.com/SoundMatt/RELAY/v2" ) // --- unit tests for schema validators --- @@ -312,6 +316,181 @@ func TestRunConformNoArgs(t *testing.T) { } } +// --- tests for --manifest / buildManifest (spec §17.2, Requirement 15) --- + +//fusa:test REQ-RELAY-095 +func TestBuildManifestSelf(t *testing.T) { + bin := buildTestBinary(t) + m := buildManifest(bin) + + if m.Kind != "relay-conform-manifest" { + t.Errorf("Kind = %q, want %q", m.Kind, "relay-conform-manifest") + } + if m.ManifestVersion != "relay-conform/1" { + t.Errorf("ManifestVersion = %q, want %q", m.ManifestVersion, "relay-conform/1") + } + if m.Tool == "" { + t.Error("Tool is empty") + } + if m.BinaryVersion == "" { + t.Error("BinaryVersion is empty") + } + if m.SpecVersion == "" { + t.Error("SpecVersion is empty") + } + if len(m.Requirements) != 15 { + t.Fatalf("len(Requirements) = %d, want 15", len(m.Requirements)) + } + for i, r := range m.Requirements { + if r.ID != i+1 { + t.Errorf("Requirements[%d].ID = %d, want %d", i, r.ID, i+1) + } + if r.Status == "" { + t.Errorf("Requirements[%d] (id=%d) has empty status", i, r.ID) + } + if r.Verifier == "" { + t.Errorf("Requirements[%d] (id=%d) has empty verifier", i, r.ID) + } + // §17.2: a manifest MUST NOT report PASS for a requirement relay + // conform cannot actually observe. Only 1, 6, 7, 12 may be PASS/FAIL; + // everything else must never be reported PASS. + observable := map[int]bool{1: true, 6: true, 7: true, 12: true} + if !observable[r.ID] && r.Status == statusPass { + t.Errorf("Requirements[%d] (id=%d) reports PASS but is not in the observable set", i, r.ID) + } + } + // relay must conform to itself, so the overall manifest result must be PASS. + if m.Overall != statusPass { + t.Errorf("Overall = %s, want %s (relay must conform to itself)", m.Overall, statusPass) + } +} + +//fusa:test REQ-RELAY-095 +func TestBuildManifestNotObservableDefaults(t *testing.T) { + bin := buildTestBinary(t) + m := buildManifest(bin) + + wantNotObservable := []int{2, 3, 4, 5, 8, 9, 10, 11, 13, 14, 15} + byID := map[int]manifestRequirement{} + for _, r := range m.Requirements { + byID[r.ID] = r + } + for _, id := range wantNotObservable { + r, ok := byID[id] + if !ok { + t.Errorf("requirement %d missing from manifest", id) + continue + } + if r.Status != statusNotObservable { + t.Errorf("requirement %d status = %s, want %s", id, r.Status, statusNotObservable) + } + } +} + +//fusa:test REQ-RELAY-095 +func TestBuildManifestCapabilitiesSHA256(t *testing.T) { + bin := buildTestBinary(t) + capsJSON, err := runBinaryCommand(bin, []string{"capabilities"}) + if err != nil { + t.Fatalf("capabilities: %v", err) + } + sum := sha256.Sum256(capsJSON) + want := hex.EncodeToString(sum[:]) + + m := buildManifest(bin) + if m.CapabilitiesSHA256 != want { + t.Errorf("CapabilitiesSHA256 = %q, want %q (independently computed sha256 of raw capabilities bytes)", m.CapabilitiesSHA256, want) + } +} + +//fusa:test REQ-RELAY-095 +func TestBuildManifestSchemaValid(t *testing.T) { + bin := buildTestBinary(t) + m := buildManifest(bin) + + data, err := json.Marshal(m) + if err != nil { + t.Fatalf("json.Marshal(manifest): %v", err) + } + var asAny interface{} + if err := json.Unmarshal(data, &asAny); err != nil { + t.Fatalf("re-unmarshal manifest JSON: %v", err) + } + schemaJSON, err := relay.Schema("relay-conform-manifest") + if err != nil { + t.Fatalf("no embedded relay-conform-manifest schema: %v", err) + } + violations := validateSchema(schemaJSON, asAny) + if len(violations) != 0 { + t.Errorf("manifest does not conform to relay-conform-manifest schema: %v\nmanifest: %s", violations, data) + } +} + +//fusa:test REQ-RELAY-095 +func TestBuildManifestFailPropagatesToOverall(t *testing.T) { + bin := buildFailingBinary(t) + m := buildManifest(bin) + + // version/capabilities/status all fail against a binary that always + // exits non-zero, so requirements 1, 6, 7 must be FAIL and Overall must + // be FAIL. + byID := map[int]manifestRequirement{} + for _, r := range m.Requirements { + byID[r.ID] = r + } + for _, id := range []int{1, 6, 7} { + if got := byID[id].Status; got != statusFail { + t.Errorf("requirement %d status = %s, want %s", id, got, statusFail) + } + } + if m.Overall != statusFail { + t.Errorf("Overall = %s, want %s", m.Overall, statusFail) + } +} + +//fusa:test REQ-RELAY-095 +func TestRunConformManifestFlag(t *testing.T) { + bin := buildTestBinary(t) + var out bytes.Buffer + var errbuf bytes.Buffer + err := runConform(&out, &errbuf, []string{"--manifest", bin}) + if err != nil { + t.Fatalf("relay conform --manifest relay: unexpected error: %v\noutput: %s", err, out.String()) + } + var m conformManifest + if jsonErr := json.Unmarshal(out.Bytes(), &m); jsonErr != nil { + t.Fatalf("conform --manifest output is not valid JSON: %v\noutput: %s", jsonErr, out.String()) + } + if m.Overall != statusPass { + t.Errorf("Overall = %s, want %s", m.Overall, statusPass) + } + if len(m.Requirements) != 15 { + t.Errorf("len(Requirements) = %d, want 15", len(m.Requirements)) + } +} + +//fusa:test REQ-RELAY-095 +func TestRunConformManifestFlagFail(t *testing.T) { + bin := buildFailingBinary(t) + var out bytes.Buffer + var errbuf bytes.Buffer + err := runConform(&out, &errbuf, []string{"--manifest", bin}) + if err == nil { + t.Fatalf("expected non-nil error for FAIL manifest, got nil\noutput: %s", out.String()) + } + var code exitCode + if !errors.As(err, &code) || int(code) != 1 { + t.Errorf("expected exitCode(1) for FAIL manifest, got %v", err) + } + var m conformManifest + if jsonErr := json.Unmarshal(out.Bytes(), &m); jsonErr != nil { + t.Fatalf("conform --manifest output is not valid JSON: %v\noutput: %s", jsonErr, out.String()) + } + if m.Overall != statusFail { + t.Errorf("Overall = %s, want %s", m.Overall, statusFail) + } +} + func countBySeverity(fs []conformFinding, sev conformSeverity) int { n := 0 for _, f := range fs { diff --git a/cmd/relay/main.go b/cmd/relay/main.go index 1d1fb5f..13c2c0d 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go @@ -18,7 +18,7 @@ import ( relay "github.com/SoundMatt/RELAY/v2" ) -const toolVersion = "2.2.0" +const toolVersion = "2.3.0" func main() { if err := run(os.Stdout, os.Stderr, os.Args[1:]); err != nil { diff --git a/spec/CHANGELOG.md b/spec/CHANGELOG.md index b8e8dcf..53650b6 100644 --- a/spec/CHANGELOG.md +++ b/spec/CHANGELOG.md @@ -1,5 +1,40 @@ # RELAY Spec Changelog +## v2.3 — 2026-08-21 (MINOR — new §17 conformance requirement) + +- **New §17 Requirement 15 — Conformance manifest.** The implementation + MUST commit a `relay-conform/1` manifest (new §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`. New MINOR-worthy Requirement, following the same precedent as + Requirements 13 (v2.1) and 14 (v2.2): a numbered §17 requirement is + MINOR-worthy even where `relay conform` cannot fully observe it. +- **New §17.2 — Conformance manifest (`relay-conform/1`).** Defines the + manifest JSON shape (`kind`, `manifest_version`, `tool`, + `binary_version`, `spec_version`, `git_sha`, `capabilities_sha256`, + `requirements[]`, `overall`) and a 4-value status vocabulary + (`PASS`/`FAIL`/`SHAPE_ONLY`/`NOT_OBSERVABLE`) deliberately distinct from + `relay conform`'s existing `PASS`/`WARN`/`FAIL` finding severities: a + manifest MUST NOT report `PASS` for a requirement `relay conform` + cannot actually observe. New embedded schema + `spec/schemas/relay-conform-manifest.json`. +- **New optional `commit` field on the §12.1 version document** — the git + SHA (short or full) the binary was built from, if the build embeds one. + Absent or empty when unavailable; this is not itself a conformance + failure, but it is what lets a manifest populate its own `git_sha` + field. Backward-compatible: not added to the schema's `required` array. +- **New §20.1 CI gate**: regenerate the manifest and fail the job on any + diff from the committed copy, or on any requirement entry whose + `status` is `FAIL`. +- **Reference implementation**: `relay conform --manifest ` in + this repo's own CLI (`cmd/relay/conform.go`). Only Requirements 1, 6, 7, + 12 get real `PASS`/`FAIL`/`SHAPE_ONLY` statuses, mirroring §17's + existing narrative of what `relay conform` can observe black-box; + everything else — including Requirement 15's own manifest-generation + entry — is honestly `NOT_OBSERVABLE`, per this release's own new rule. + `SpecVersion` bumped to `2.3`. Closes [NEW-SPEC-1]. + ## v2.2.4 — 2026-08-21 (doc addition; no normative change to existing conformant implementations) - **Fixed a real spec-internal inconsistency**: §8.3's Go `CalcChecksum` diff --git a/spec/relay-spec.md b/spec/relay-spec.md index d2436d9..d9831c0 100644 --- a/spec/relay-spec.md +++ b/spec/relay-spec.md @@ -1,4 +1,4 @@ -# RELAY Specification — v2.2 +# RELAY Specification — v2.3 **Real-time Embedded Link Abstraction Yoke** @@ -1182,14 +1182,20 @@ 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.2", + "spec_version": "2.3", "language": "go", - "runtime": "go1.25.0" + "runtime": "go1.25.0", + "commit": "a1b2c3d4" } ``` `language` MUST be one of: `"go"`, `"cpp"`, `"rust"`, `"c"`. +`commit` is OPTIONAL: the git SHA (short or full) the binary was built from, if +the build embeds one. Absent or empty when unavailable — this is not itself a +conformance failure, but §17.2's conformance manifest cannot populate its own +`git_sha` field without it. + ### 12.2 Capabilities document ` capabilities`: @@ -1201,7 +1207,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.2", + "spec_version": "2.3", "commands": ["version", "capabilities", "status", "connect", "send", "subscribe"], "transports": ["socketcan", "virtual"], "features": ["fd", "isotp", "j1939"], @@ -1272,7 +1278,7 @@ $ go-can version --format json "protocol": "CAN", "protocol_int": 1, "version": "1.2.3", - "spec_version": "2.2", + "spec_version": "2.3", "language": "go", "runtime": "go1.25.0" } @@ -1284,7 +1290,7 @@ $ go-can capabilities "protocol": "CAN", "protocol_int": 1, "version": "1.2.3", - "spec_version": "2.2", + "spec_version": "2.3", "commands": ["version", "capabilities", "status", "connect", "send", "subscribe"], "transports": ["socketcan", "virtual"], "features": ["fd", "isotp", "j1939"], @@ -1360,11 +1366,11 @@ LABEL org.opencontainers.image.licenses="MPL-2.0" LABEL io.relay.tool="" LABEL io.relay.language="go|cpp|rust|c" LABEL io.relay.binary="" -LABEL io.relay.spec-version="2.2" +LABEL io.relay.spec-version="2.3" ``` The `io.relay.spec-version` label MUST always match the value of `SpecVersion` -exported by the package (§17.12 / §19.4). The `"2.2"` shown above is an example; +exported by the package (§17.12 / §19.4). The `"2.3"` shown above is an example; update it on each spec minor release. The project directory is mounted at `/project` by convention: @@ -2155,6 +2161,7 @@ An implementation is **RELAY-conformant** if and only if: 12. **SpecVersion constant.** Package exports `SpecVersion` equal to the spec version being targeted. The authoritative current value is defined in §19.4 (`spec/version.json`). 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`. `relay conform ` is a **black-box CLI tool**: it can only observe what the built binary's `version`/`capabilities`/`status` commands print, not the @@ -2192,7 +2199,13 @@ whether `spec_version` is bound to `relay.SpecVersion`, checked against `spec/version.json` by a script, or simply typed once and forgotten — `relay conform` can only shape-check the printed string (per Requirement 12 above), never how it was produced, so this too is verified by the -implementation's own CI, not `relay conform`. +implementation's own CI, not `relay conform`. Requirement 15 (conformance +manifest) is split down the middle: `relay conform --manifest` itself +*generates* the manifest content — that part is fully within `relay +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. **Requirement-to-verifier lookup table**, collecting the narrative above into one place: @@ -2213,6 +2226,7 @@ into one place: | 12 | SpecVersion constant | `relay conform` (`spec_version` shape check) | Shape only — value's correctness cannot be observed | | 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 | ### 17.1 Wire-format regression coverage (recommended) @@ -2248,6 +2262,67 @@ an implementation's internal test suite, so there is no mechanism to gate conformance on it — the same reason Requirements 2–5 and 8–11 above are MUSTs the CLI itself cannot verify. +### 17.2 Conformance manifest (`relay-conform/1`) + +§17's own requirement list is prose; ecosystem audits found that prose gap +exploited in practice — a port's CI can print "conformant" without any +machine-checkable artifact actually gating on it (THEME-I), while its +`capabilities` document and declared `spec_version` silently drift from +what the binary really ships (THEME-A, THEME-B). This subsection converts +the requirement-to-verifier table in §17 above from prose into a pinned, +diffable artifact. + +`relay conform --manifest ` emits a `relay-conform/1` document: + +```json +{ + "kind": "relay-conform-manifest", + "manifest_version": "relay-conform/1", + "tool": "go-can", + "binary_version": "1.2.3", + "spec_version": "2.3", + "git_sha": "a1b2c3d4", + "capabilities_sha256": "e3b0c44298fc1c14...", + "requirements": [ + {"id": 1, "status": "PASS", "verifier": "relay conform"}, + {"id": 2, "status": "NOT_OBSERVABLE", "verifier": "implementation test suite"}, + {"id": 7, "status": "PASS", "verifier": "relay conform"}, + {"id": 12, "status": "SHAPE_ONLY", "verifier": "relay conform"} + ], + "overall": "PASS" +} +``` + +`git_sha` is the `commit` field from the `version` document (§12.1) verbatim, +or `""` if the binary didn't report one. `capabilities_sha256` is the +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, +extended as this document's own requirement list grows). Each entry's +`status` is one of: + +| Status | Meaning | +|---|---| +| `PASS` | `relay conform` directly verified this requirement and it held. | +| `FAIL` | `relay conform` directly verified this requirement and it failed. | +| `SHAPE_ONLY` | `relay conform` validated the document shape but cannot verify the underlying value's correctness (Requirement 12's own limit, §17). | +| `NOT_OBSERVABLE` | This requirement is a source-level or CI-process fact `relay conform`'s black-box CLI cannot observe at all (per the requirement-to-verifier table, §17) — verified by the implementation's own test suite or CI instead. | + +A manifest MUST NOT report `PASS` for a requirement `relay conform` cannot +actually observe — that would misrepresent black-box coverage as +verification and recreate exactly the "prints conformant but isn't gated" +gap this subsection exists to close. `overall` is `FAIL` if any entry is +`FAIL`, else `PASS`. + +Per Requirement 15 (§17), a conformant implementation MUST commit this +manifest to its repository, and CI MUST regenerate it on every change, +failing the build on any diff from the committed copy or any `FAIL` entry. +This is deliberately narrower than mandating a specific commit path or +file name — implementations already vary in repository layout — but the +manifest itself, once committed, is what CI diffs. + --- ## 18. Language Bindings @@ -2954,11 +3029,11 @@ clarifications and fixes in PATCH releases. `spec/version.json` is authoritative. The spec document title is informational. -Current version: **v2.2** +Current version: **v2.3** -**Go:** `const SpecVersion = "2.2"` (update in implementations targeting v2.2) -**C++:** `constexpr std::string_view kRelaySpecVersion = "2.2";` -**Rust:** `pub const RELAY_SPEC_VERSION: &str = "2.2";` +**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";` --- @@ -2985,6 +3060,10 @@ every pull request, run and gate on: 3. **Behavioural conformance** — once `convert` is implemented (§20.3), `relay interop` against the reference MUST report the implementation EQUIVALENT for every golden vector of its protocol. +4. **Conformance manifest** (§17.2, Requirement 15) — regenerate the + `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`. Releases MUST be tagged only from a commit whose CI is fully green. diff --git a/spec/schemas/cli-version.json b/spec/schemas/cli-version.json index 0500672..d0cdb0a 100644 --- a/spec/schemas/cli-version.json +++ b/spec/schemas/cli-version.json @@ -34,6 +34,10 @@ "runtime": { "type": "string", "description": "Runtime version (e.g. \"go1.25.0\", \"clang-18\")." + }, + "commit": { + "type": "string", + "description": "Git SHA (short or full) the binary was built from. Optional; absent or empty when unavailable." } }, "required": ["tool", "version", "spec_version", "language", "runtime"], diff --git a/spec/schemas/relay-conform-manifest.json b/spec/schemas/relay-conform-manifest.json new file mode 100644 index 0000000..36edf82 --- /dev/null +++ b/spec/schemas/relay-conform-manifest.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SoundMatt/RELAY/spec/schemas/relay-conform-manifest.json", + "title": "RELAY conformance manifest", + "description": "JSON output of `relay conform --manifest ` (spec §17.2, Requirement 15).", + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "relay-conform-manifest" + }, + "manifest_version": { + "type": "string", + "const": "relay-conform/1" + }, + "tool": { + "type": "string", + "description": "Binary name, from the version document (§12.1)." + }, + "binary_version": { + "type": "string", + "description": "Semantic version of the binary, from the version document (§12.1)." + }, + "spec_version": { + "type": "string", + "description": "Declared spec_version, from the version document (§12.1)." + }, + "git_sha": { + "type": "string", + "description": "The version document's optional commit field, verbatim, or \"\" if absent." + }, + "capabilities_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "SHA-256 (lowercase hex) of the raw capabilities document bytes." + }, + "requirements": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "description": "The §17 requirement number." + }, + "status": { + "type": "string", + "enum": ["PASS", "FAIL", "SHAPE_ONLY", "NOT_OBSERVABLE"] + }, + "verifier": { + "type": "string", + "description": "What verified this requirement, e.g. \"relay conform\" or \"implementation test suite\"." + } + }, + "required": ["id", "status", "verifier"], + "additionalProperties": false + }, + "description": "One entry per §17 requirement (1-15, extended as the requirement list grows)." + }, + "overall": { + "type": "string", + "enum": ["PASS", "FAIL"], + "description": "FAIL if any requirements entry is FAIL, else PASS." + } + }, + "required": ["kind", "manifest_version", "tool", "binary_version", "spec_version", "git_sha", "capabilities_sha256", "requirements", "overall"], + "additionalProperties": false +} diff --git a/spec/version.json b/spec/version.json index e3dac4e..9f9461e 100644 --- a/spec/version.json +++ b/spec/version.json @@ -1,5 +1,5 @@ { - "version": "2.2", + "version": "2.3", "date": "2026-08-20", "status": "stable", "networks": [ diff --git a/version.go b/version.go index 6c7734a..b39fbff 100644 --- a/version.go +++ b/version.go @@ -8,4 +8,4 @@ package relay // Every RELAY-conformant implementation must export this constant (spec §17 req 12). // //fusa:req REQ-RELAY-020 -const SpecVersion = "2.2" +const SpecVersion = "2.3" diff --git a/version_test.go b/version_test.go index 0cadf55..12568b4 100644 --- a/version_test.go +++ b/version_test.go @@ -8,7 +8,7 @@ import "testing" //fusa:test REQ-RELAY-020 func TestSpecVersion(t *testing.T) { - if SpecVersion != "2.2" { - t.Errorf("SpecVersion = %q, want %q", SpecVersion, "2.2") + if SpecVersion != "2.3" { + t.Errorf("SpecVersion = %q, want %q", SpecVersion, "2.3") } } From 5b62ffbbbc8f216fbc14b61ad424e9403fbe976b Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:10:55 -0700 Subject: [PATCH 2/2] fix: gofmt cmd/relay/conform.go (CI Lint failure) Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- cmd/relay/conform.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cmd/relay/conform.go b/cmd/relay/conform.go index bf0c46f..5454200 100644 --- a/cmd/relay/conform.go +++ b/cmd/relay/conform.go @@ -177,15 +177,15 @@ func conformBinary(binary string, strict bool) conformResult { type manifestStatus string const ( - statusPass manifestStatus = "PASS" - statusFail manifestStatus = "FAIL" - statusShapeOnly manifestStatus = "SHAPE_ONLY" - statusNotObservable manifestStatus = "NOT_OBSERVABLE" - verifierRelayConform = "relay conform" - verifierTestSuite = "implementation test suite" - verifierBuildAndTest = "implementation's own build process and test suite" - verifierCI = "implementation's own CI" - verifierManifestSplit = "relay conform (manifest content); implementation's own CI (regenerate + diff)" + statusPass manifestStatus = "PASS" + statusFail manifestStatus = "FAIL" + statusShapeOnly manifestStatus = "SHAPE_ONLY" + statusNotObservable manifestStatus = "NOT_OBSERVABLE" + verifierRelayConform = "relay conform" + verifierTestSuite = "implementation test suite" + verifierBuildAndTest = "implementation's own build process and test suite" + verifierCI = "implementation's own CI" + verifierManifestSplit = "relay conform (manifest content); implementation's own CI (regenerate + diff)" ) // manifestRequirement is one entry in a relay-conform/1 manifest's @@ -199,15 +199,15 @@ type manifestRequirement struct { // conformManifest is the relay-conform/1 manifest document (spec §17.2, // §17 Requirement 15). type conformManifest struct { - Kind string `json:"kind"` - ManifestVersion string `json:"manifest_version"` - Tool string `json:"tool"` - BinaryVersion string `json:"binary_version"` - SpecVersion string `json:"spec_version"` - GitSHA string `json:"git_sha"` - CapabilitiesSHA256 string `json:"capabilities_sha256"` - Requirements []manifestRequirement `json:"requirements"` - Overall manifestStatus `json:"overall"` + Kind string `json:"kind"` + ManifestVersion string `json:"manifest_version"` + Tool string `json:"tool"` + BinaryVersion string `json:"binary_version"` + SpecVersion string `json:"spec_version"` + GitSHA string `json:"git_sha"` + CapabilitiesSHA256 string `json:"capabilities_sha256"` + Requirements []manifestRequirement `json:"requirements"` + Overall manifestStatus `json:"overall"` } // buildManifest generates a relay-conform/1 manifest (spec §17.2) for one