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
48 changes: 48 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,54 @@
"asil": "ASIL-B",
"rationale": "go-LIN uses time.After for slot delays; clock accuracy below 1 ms will cause schedule timing jitter.",
"tags": ["seooc", "assumption", "timing"]
},
{
"id": "REQ-ADAPT-001",
"title": "Adapt returns a LIN relay.Node",
"description": "Adapt shall return a non-nil relay.Node whose Protocol method reports relay.LIN.",
"asil": "ASIL-B",
"rationale": "RELAY §10.3/§13.7: the protocol-agnostic application layer must observe the correct protocol tag to route LIN traffic.",
"tags": ["adapt", "relay"]
},
{
"id": "REQ-ADAPT-002",
"title": "linNode.Send publishes payload for a valid frame ID",
"description": "linNode.Send shall publish msg.Payload as the slave response for the frame ID parsed from msg.ID when the ID is in range 0-63.",
"asil": "ASIL-B",
"rationale": "RELAY §10.3: the adapter egress path must faithfully forward application payloads onto the LIN bus.",
"tags": ["adapt", "relay"]
},
{
"id": "REQ-ADAPT-003",
"title": "linNode.Send rejects an out-of-range frame ID",
"description": "linNode.Send shall return a non-nil error and shall not publish when msg.ID is not a decimal string in range 0-63.",
"asil": "ASIL-B",
"rationale": "SG-05: an invalid identifier supplied through the adapter must not be transmitted.",
"tags": ["adapt", "relay", "validation"]
},
{
"id": "REQ-ADAPT-004",
"title": "linNode.Subscribe converts frames to relay.Message",
"description": "linNode.Subscribe shall return a channel that yields one relay.Message per received frame, each carrying the frame's canonical ToMessage() conversion.",
"asil": "ASIL-B",
"rationale": "RELAY §10.5: the adapter ingress path must deliver every received frame to the application as a canonical envelope.",
"tags": ["adapt", "relay"]
},
{
"id": "REQ-ADAPT-005",
"title": "linNode.Close closes the underlying bus",
"description": "linNode.Close shall close the wrapped Bus and return its error unchanged.",
"asil": "ASIL-B",
"rationale": "RELAY §6 lifecycle: closing the node must release the underlying transport so no further frames are processed.",
"tags": ["adapt", "relay", "lifecycle"]
},
{
"id": "REQ-MOCK-001",
"title": "mock.New returns a MasterBus-satisfying bus",
"description": "mock.New shall return a non-nil bus that satisfies lin.MasterBus (Publish, Subscribe, SendHeader, SetSchedule, Close).",
"asil": "ASIL-B",
"rationale": "RELAY §7 rule 4 / §13.7: a mandatory mock transport must implement the full bus contract so applications can be tested without hardware.",
"tags": ["mock", "relay"]
}
]
}
95 changes: 89 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,60 @@ jobs:
- name: FuzzProtectUnwrap (safety)
run: go test -fuzz=^FuzzProtectUnwrap$ -fuzztime=200000x ./safety/...

# ── go-FuSa safety checks ─────────────────────────────────────────────────
# Pinned to v0.30.0; exits 1 on any ERROR-level finding.
# To upgrade: install the new version locally, run 'gofusa check ./...',
# fix all findings, then bump the @version pin below.
# ── RELAY conformance gate (spec §12 / §17 / §20.1.1) ─────────────────────
# Validates the built CLI's version/capabilities/status documents against the
# §12 JSON Schemas embedded in the relay binary. --strict fails on FAIL or WARN.
# Pin the relay version so spec bumps are an explicit upgrade.
relay-conform:
name: RELAY conform (--strict)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-go@v6
with:
go-version: "1.25"

- name: Install relay CLI
run: go install github.com/SoundMatt/RELAY/cmd/relay@v1.10.0

- name: Build CLI
run: go build -o /tmp/go-lin ./cmd/go-lin

- name: relay conform --strict
run: relay conform --strict /tmp/go-lin

# ── RELAY interop behavioural conformance (spec §11.2 / §20.2) ────────────
# Drives the CLI's `convert` command with the LIN golden vectors and confirms
# its relay.Message output is EQUIVALENT to the reference implementation.
relay-interop:
name: RELAY interop (LIN)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-go@v6
with:
go-version: "1.25"

- name: Install relay CLI
run: go install github.com/SoundMatt/RELAY/cmd/relay@v1.10.0

- name: Build CLI
run: go build -o /tmp/go-lin ./cmd/go-lin

- name: relay interop --protocol LIN
run: relay interop --protocol LIN /tmp/go-lin

# ── go-FuSa full safety lifecycle (spec §20.1.2) ──────────────────────────
# Pinned to v0.30.0. §20 makes the full lifecycle normative: every change must
# pass check (ERROR gate), 100% requirement traceability, cybersecurity
# analysis, dependency vulnerability scan, and tool qualification. The
# remaining steps generate evidence artifacts (verify/fmea/release) and are
# non-gating. To upgrade: install the new version locally, run the gating
# commands, fix all findings, then bump the @version pin below.
gofusa:
name: go-FuSa safety check (v0.30.0)
name: go-FuSa full lifecycle (v0.30.0)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -105,5 +153,40 @@ jobs:
- name: Install go-FuSa
run: go install github.com/SoundMatt/go-FuSa/cmd/gofusa@v0.30.0

- name: gofusa check
- name: gofusa check (gate on ERROR findings)
run: gofusa check ./...

- name: gofusa trace (gate on 100% requirement traceability)
run: gofusa trace -req-coverage 100

- name: gofusa cyber (cybersecurity analysis)
run: gofusa cyber

- name: gofusa vuln (dependency vulnerability scan)
run: gofusa vuln

- name: gofusa qualify (tool qualification suite)
run: gofusa qualify

- name: gofusa verify (test evidence bundle)
run: gofusa verify || true

- name: gofusa tara (threat analysis, ISO 21434)
run: gofusa tara || true

- name: gofusa fmea (dFMEA)
run: gofusa fmea || true

- name: gofusa release (SBOM + build provenance, SLSA)
run: gofusa release || true

- name: gofusa audit-pack (bundle all evidence)
run: gofusa audit-pack --output gofusa-audit-pack.zip || true

- name: Upload safety evidence
if: always()
uses: actions/upload-artifact@v6
with:
name: gofusa-safety-evidence
path: gofusa-audit-pack.zip
if-no-files-found: ignore
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ jobs:
- name: Regenerate dFMEA table
run: gofusa fmea -cyber

- name: Regenerate TARA (threat analysis, ISO 21434)
run: gofusa tara

- name: Generate test evidence bundle (closes safety-case verify gap)
run: gofusa verify || true

- name: Regenerate safety case
run: gofusa safety-case

Expand All @@ -46,6 +52,7 @@ jobs:
run: |
TAG="${{ github.ref_name }}"
git add fmea.csv fmea.json \
tara.json tara.md \
safety-case.json safety-case.md safety-case.mermaid \
sbom.json provenance.json artifact-manifest.json
if git diff --cached --quiet; then
Expand Down
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*.so
*.dylib
dist/
/go-lin
/lintool

# Test output
*.out
Expand All @@ -21,8 +23,13 @@ go.work.sum
*.swp
*.swo

# go-FuSa generated (regenerated by gofusa release on tag)
# go-FuSa transient gate reports (regenerated by the CI lifecycle each run)
check-report.json
cyber-report.json
qualify-report.json
vuln.json
gofusa-audit-pack.zip
.fusa-evidence.json

# OS
.DS_Store
Expand Down
9 changes: 9 additions & 0 deletions adapt.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (

// Adapt wraps bus as a relay.Node for use with protocol-agnostic applications.
// Adapt does not block; it does not connect.
//
//fusa:req REQ-ADAPT-001
func Adapt(bus Bus) relay.Node {
return &linNode{bus: bus}
}
Expand All @@ -24,12 +26,16 @@ type linNode struct {
bus Bus
}

//fusa:req REQ-ADAPT-001
func (n *linNode) Protocol() relay.Protocol {
return relay.LIN
}

// Send publishes msg.Payload as a slave response for the frame ID in msg.ID.
// msg.ID must be a decimal string in range 0–63.
//
//fusa:req REQ-ADAPT-002
//fusa:req REQ-ADAPT-003
func (n *linNode) Send(ctx context.Context, msg relay.Message) error {
id, err := strconv.ParseUint(msg.ID, 10, 8)
if err != nil || id > LINMaxID {
Expand All @@ -39,6 +45,8 @@ func (n *linNode) Send(ctx context.Context, msg relay.Message) error {
}

// Subscribe returns a channel of relay.Message envelopes for all received frames.
//
//fusa:req REQ-ADAPT-004
func (n *linNode) Subscribe(opts ...relay.SubscriberOption) (<-chan relay.Message, error) {
cfg := relay.ApplySubscriberOpts(opts)
ch := make(chan relay.Message, cfg.ChanDepth(64))
Expand Down Expand Up @@ -75,6 +83,7 @@ func (n *linNode) Subscribe(opts ...relay.SubscriberOption) (<-chan relay.Messag
return ch, nil
}

//fusa:req REQ-ADAPT-005
func (n *linNode) Close() error {
return n.bus.Close()
}
108 changes: 108 additions & 0 deletions adapt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2026 Matt Jones. All rights reserved.
// 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 lin_test

import (
"context"
"testing"
"time"

relay "github.com/SoundMatt/RELAY"
lin "github.com/SoundMatt/go-LIN"
"github.com/SoundMatt/go-LIN/virtual"
)

// ── RELAY adapter (spec §10.3 / §13.7) ────────────────────────────────────────

//fusa:test REQ-ADAPT-001
func TestAdapt_protocolIsLIN(t *testing.T) {
bus, err := virtual.New()
if err != nil {
t.Fatalf("virtual.New: %v", err)
}
defer bus.Close()

node := lin.Adapt(bus)
if node == nil {
t.Fatal("Adapt returned nil")
}
if node.Protocol() != relay.LIN {
t.Errorf("Protocol() = %v, want relay.LIN", node.Protocol())
}
}

//fusa:test REQ-ADAPT-002
//fusa:test REQ-ADAPT-004
func TestAdapt_sendThenReceive(t *testing.T) {
bus, err := virtual.New()
if err != nil {
t.Fatalf("virtual.New: %v", err)
}
node := lin.Adapt(bus)
defer node.Close()

ch, err := node.Subscribe()
if err != nil {
t.Fatalf("Subscribe: %v", err)
}

payload := []byte{0xAA, 0xBB, 0xCC, 0xDD}
if err := node.Send(context.Background(), relay.Message{ID: "16", Payload: payload}); err != nil {
t.Fatalf("Send: %v", err)
}
// Send registers the slave response; trigger one exchange so the frame is
// delivered to subscribers.
if _, err := bus.SendHeader(context.Background(), 16); err != nil {
t.Fatalf("SendHeader: %v", err)
}

select {
case msg := <-ch:
if msg.Protocol != relay.LIN {
t.Errorf("msg.Protocol = %v, want LIN", msg.Protocol)
}
if msg.ID != "16" {
t.Errorf("msg.ID = %q, want \"16\"", msg.ID)
}
if string(msg.Payload) != string(payload) {
t.Errorf("msg.Payload = %x, want %x", msg.Payload, payload)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for adapted message")
}
}

//fusa:test REQ-ADAPT-003
func TestAdapt_sendRejectsBadID(t *testing.T) {
bus, err := virtual.New()
if err != nil {
t.Fatalf("virtual.New: %v", err)
}
node := lin.Adapt(bus)
defer node.Close()

for _, id := range []string{"64", "not-a-number", "-1"} {
if err := node.Send(context.Background(), relay.Message{ID: id, Payload: []byte{1}}); err == nil {
t.Errorf("Send(ID=%q) = nil error, want rejection", id)
}
}
}

//fusa:test REQ-ADAPT-005
func TestAdapt_closeClosesBus(t *testing.T) {
bus, err := virtual.New()
if err != nil {
t.Fatalf("virtual.New: %v", err)
}
node := lin.Adapt(bus)
if err := node.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// The underlying bus is closed: a further Publish must fail.
if err := bus.Publish(1, []byte{1}); err == nil {
t.Error("Publish after node.Close() = nil error, want closed error")
}
}
10 changes: 5 additions & 5 deletions artifact-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
"tool": "go-FuSa",
"toolVersion": "0.30.0",
"language": "go",
"generatedAt": "2026-06-17T14:31:08.909144783Z",
"generatedAt": "2026-06-19T19:27:27.299813Z",
"format": "x-FuSa manifest v1",
"artifacts": [
{
"path": "/home/runner/work/go-LIN/go-LIN/sbom.json",
"sha256": "70d4a8c18eb0c3a9503c88ca2b5badfe39ca097d7ce89e4d5c421808044a300f"
"path": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/sbom.json",
"sha256": "0f3af662af7a0b96cd87813d5704d73b9dc008af6829a6fc8cc1ca3fee3fccb0"
},
{
"path": "/home/runner/work/go-LIN/go-LIN/provenance.json",
"sha256": "a9a45ab826bfcfee13e6e52f3cea0be22545d361ecb38bb454f531e2895d652e"
"path": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/provenance.json",
"sha256": "58f4cad2d2f2256a2296773386f410bb675565d5af2812239359ed88db1c56bb"
}
]
}
Loading
Loading