From b5fe6717c28c846d3a8b6068f2cf65c4c010a409 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:44:48 -0700 Subject: [PATCH 1/3] =?UTF-8?q?fix(cli):=20v0.39.0=20=E2=80=94=20carry=20f?= =?UTF-8?q?orward=20=C2=A71.6.2=20attestation=20on=20regeneration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §1.6.2 was updated (spec v1.15.0) to make attestation carry-forward a MUST: before an artifact-producing command rebuilds its output, it must load any existing attestation from the prior saved output file and carry it forward, rather than discarding it. gofusa fmea/tara/safety-case/sas each unconditionally overwrote their output with a brand-new report built from scratch — none loaded the existing file first or copied forward its attestation field, so a hand-added, valid, reviewed attestation was silently destroyed the moment the command was re-run, even when the artifact's substantive content hadn't changed (go-FuSa#57). A project that adopted --require-attestation/ --strict per the spec's suggested CI shape would find their review wiped on every regeneration, defeating the whole mechanism. Add carryForwardAttestation (cmd/gofusa/helpers.go), which loads the "attestation" key from a prior output file — working uniformly across fmea.json/tara.json/safety-case.json/sas.json without needing each artifact's full schema — and wire it into each of the four commands right before they overwrite their JSON output. Staleness continues to fall out automatically: a carried-forward contentHash that no longer matches the freshly-computed content hash means AttestationValid (via stubcheck.AttestationSuppresses) treats the attestation as not currently suppressing, never that it silently vanished. Signed-off-by: Matt Jones Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 6 + CHANGELOG.md | 20 +++ cmd/gofusa/cmd_attestation_test.go | 265 +++++++++++++++++++++++++++++ cmd/gofusa/cmd_fmea.go | 8 +- cmd/gofusa/cmd_safetycase.go | 9 +- cmd/gofusa/cmd_sas.go | 14 ++ cmd/gofusa/cmd_tara.go | 8 +- cmd/gofusa/helpers.go | 32 ++++ fusa.go | 2 +- 9 files changed, 360 insertions(+), 4 deletions(-) create mode 100644 cmd/gofusa/cmd_attestation_test.go diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 2983d83..01060cf 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -2454,6 +2454,12 @@ "text": "gateContentQuality shall scan an artifact's qualitative fields for FUSA-STUB001/002 findings, print them, and return ExitGateFail when a placeholder match exists or when an unsuppressed blanket-fallback match exists and strict gating is requested.", "standard": "x-FuSa spec section 1.6.1" }, + { + "id": "REQ-CLI-HELPERS005", + "title": "carryForwardAttestation loads a prior output file's attestation", + "text": "carryForwardAttestation shall read the attestation field from the prior saved copy of an artifact-producing command's output file and return it, or nil if the file is absent, unreadable, malformed, or carries no attestation, so each fmea/tara/safety-case/sas command can carry a reviewed attestation forward onto its freshly-rebuilt output per x-FuSa spec section 1.6.2.", + "standard": "x-FuSa spec section 1.6.2" + }, { "id": "REQ-FMEA007", "title": "FMEA summary block with coverage metrics", diff --git a/CHANGELOG.md b/CHANGELOG.md index a927977..d50b36d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,26 @@ Dates reference the merged commit timestamp. continues to be suppressed only by a valid §1.6.2 attestation, never by disposition. +## v0.39.0 — 2026-07-28 (x-FuSa spec §1.6.2 attestation carry-forward MUST) + +### Fixed +- **§1.6.2 attestation is no longer silently wiped on every regeneration** + of `fmea.json`/`tara.json`/`safety-case.json`/`sas.json` (x-FuSa spec + v1.15.0 §1.6.2, now a MUST). `gofusa fmea`/`tara`/`safety-case`/`sas` + each unconditionally overwrote their output with a brand-new report + built from scratch, none of which loaded the existing file first or + copied forward its `attestation` field — a hand-added, valid, reviewed + attestation was discarded the moment the command was re-run, even when + nothing about the artifact's substantive content had changed + (go-FuSa#57). Each command now calls the new `carryForwardAttestation` + helper (`cmd/gofusa/helpers.go`) to load the prior saved output file's + `attestation` field and carry it forward onto the freshly-built result + before writing. Staleness continues to fall out automatically: a + carried-forward `contentHash` that no longer matches the freshly + computed content hash means `stubcheck.AttestationSuppresses` treats it + as not currently suppressing (via `fusa.AttestationValid`), never that + it vanished outright. + ## v0.36.0 — 2026-07-28 (x-FuSa spec v1.13.0/v1.14.0 — evidence-artifact schema conformance + content-quality baseline) ### Added diff --git a/cmd/gofusa/cmd_attestation_test.go b/cmd/gofusa/cmd_attestation_test.go new file mode 100644 index 0000000..0e65578 --- /dev/null +++ b/cmd/gofusa/cmd_attestation_test.go @@ -0,0 +1,265 @@ +package main + +// cmd_attestation_test.go covers x-FuSa spec §1.6.2's carry-forward MUST +// (spec v1.15.0): before an artifact-producing command rebuilds its output, +// it must load any existing attestation from the prior saved output file and +// carry it forward onto the freshly-built result, rather than discarding it +// (go-FuSa#57). Each artifact command (fmea/tara/safety-case/sas) is run +// twice: the first run produces a fresh artifact with no attestation; a +// "reviewed" attestation with a matching contentHash is then hand-added; the +// second run must preserve that attestation rather than silently wiping it. + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" + + fusa "github.com/SoundMatt/go-FuSa" +) + +//fusa:test REQ-CLI-HELPERS005 +func TestCarryForwardAttestation_AbsentFile(t *testing.T) { + if att := carryForwardAttestation(filepath.Join(t.TempDir(), "does-not-exist.json")); att != nil { + t.Errorf("expected nil for an absent file, got %+v", att) + } +} + +//fusa:test REQ-CLI-HELPERS005 +func TestCarryForwardAttestation_MalformedFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "bad.json") + if err := os.WriteFile(path, []byte("{not valid json"), 0o640); err != nil { + t.Fatal(err) + } + if att := carryForwardAttestation(path); att != nil { + t.Errorf("expected nil for a malformed file, got %+v", att) + } +} + +//fusa:test REQ-CLI-HELPERS005 +func TestCarryForwardAttestation_NoAttestationField(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "no-att.json") + if err := os.WriteFile(path, []byte(`{"entries":[]}`), 0o640); err != nil { + t.Fatal(err) + } + if att := carryForwardAttestation(path); att != nil { + t.Errorf("expected nil when no attestation key is present, got %+v", att) + } +} + +//fusa:test REQ-CLI-HELPERS005 +func TestCarryForwardAttestation_CarriesReviewedStatus(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "with-att.json") + body := `{"entries":[],"attestation":{"status":"reviewed","implementationAuthor":"auto","independentReviewer":"Jane Doe","contentHash":"sha256:deadbeef"}}` + if err := os.WriteFile(path, []byte(body), 0o640); err != nil { + t.Fatal(err) + } + att := carryForwardAttestation(path) + if att == nil { + t.Fatal("expected a non-nil attestation") + } + if att.Status != fusa.StatusReviewed || att.IndependentReviewer != "Jane Doe" { + t.Errorf("unexpected attestation: %+v", att) + } +} + +//fusa:test REQ-CLI013 +//fusa:test REQ-STUB012 +func TestRunFmea_CarriesForwardAttestation(t *testing.T) { + dir := t.TempDir() + writeSafetyFuncSource(t, dir) + + var out1, err1 bytes.Buffer + if code := runFmea([]string{"--dir", dir}, &out1, &err1); code != fusa.ExitOK { + t.Fatalf("first run: exit %d, stderr: %s", code, err1.String()) + } + + fmeaPath := filepath.Join(dir, "fmea.json") + injectReviewedAttestation(t, fmeaPath, "entries") + + var out2, err2 bytes.Buffer + if code := runFmea([]string{"--dir", dir}, &out2, &err2); code != fusa.ExitOK { + t.Fatalf("second run: exit %d, stderr: %s", code, err2.String()) + } + assertAttestationPreserved(t, fmeaPath) +} + +//fusa:test REQ-CLI019 +//fusa:test REQ-STUB012 +func TestRunTara_CarriesForwardAttestation(t *testing.T) { + dir := t.TempDir() + + var out1, err1 bytes.Buffer + if code := runTara([]string{"--dir", dir}, &out1, &err1); code != fusa.ExitOK { + t.Fatalf("first run: exit %d, stderr: %s", code, err1.String()) + } + + taraPath := filepath.Join(dir, "tara.json") + injectReviewedAttestation(t, taraPath, "threats") + + var out2, err2 bytes.Buffer + if code := runTara([]string{"--dir", dir}, &out2, &err2); code != fusa.ExitOK { + t.Fatalf("second run: exit %d, stderr: %s", code, err2.String()) + } + assertAttestationPreserved(t, taraPath) +} + +//fusa:test REQ-CLI012 +//fusa:test REQ-STUB012 +func TestRunSafetyCase_CarriesForwardAttestation(t *testing.T) { + dir := t.TempDir() + + var out1, err1 bytes.Buffer + if code := runSafetyCase([]string{"--dir", dir}, &out1, &err1); code != fusa.ExitOK { + t.Fatalf("first run: exit %d, stderr: %s", code, err1.String()) + } + + scPath := filepath.Join(dir, "safety-case.json") + injectReviewedAttestationSC(t, scPath) + + var out2, err2 bytes.Buffer + if code := runSafetyCase([]string{"--dir", dir}, &out2, &err2); code != fusa.ExitOK { + t.Fatalf("second run: exit %d, stderr: %s", code, err2.String()) + } + assertAttestationPreserved(t, scPath) +} + +//fusa:test REQ-CLI-SAS001 +//fusa:test REQ-STUB012 +func TestRunSas_CarriesForwardAttestation(t *testing.T) { + dir := t.TempDir() + sasJSONPath := filepath.Join(dir, "sas.json") + + var out1, err1 bytes.Buffer + // --format markdown (the default) writes the primary output to + // --output and the json companion alongside it — exercise that path, + // since it's the common case (`gofusa sas` with no flags). + outFile := filepath.Join(dir, "sas.md") + if code := runSas([]string{"--dir", dir, "--output", outFile}, &out1, &err1); code != fusa.ExitGateFail && code != fusa.ExitOK { + t.Fatalf("first run: unexpected exit %d, stderr: %s", code, err1.String()) + } + if _, err := os.Stat(sasJSONPath); err != nil { + t.Fatalf("sas.json companion not written: %v", err) + } + + injectReviewedAttestation(t, sasJSONPath, "deviations") + + var out2, err2 bytes.Buffer + runSas([]string{"--dir", dir, "--output", outFile}, &out2, &err2) + assertAttestationPreserved(t, sasJSONPath) +} + +// ─── shared test helpers ──────────────────────────────────────────────────── + +func writeSafetyFuncSource(t *testing.T, dir string) { + t.Helper() + src := "package main\n\n//fusa:req REQ-001\nfunc SafetyFunc() error { return nil }\n" + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } +} + +// injectReviewedAttestation reads the JSON artifact at path, computes the +// canonical content hash over its contentKey field (e.g. "entries", +// "threats", "deviations" — whichever field the command's own +// gateContentQuality call hashes), and rewrites the file with a valid +// "reviewed" attestation whose contentHash matches that field's current +// content, so a second run's regenerated content — if unchanged — carries a +// *currently-valid* (not stale) attestation forward. +func injectReviewedAttestation(t *testing.T, path, contentKey string) { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + var doc map[string]interface{} + if unmarshalErr := json.Unmarshal(data, &doc); unmarshalErr != nil { + t.Fatalf("unmarshal %s: %v", path, unmarshalErr) + } + hash, err := fusa.AttestationContentHash(doc[contentKey]) + if err != nil { + t.Fatalf("AttestationContentHash: %v", err) + } + doc["attestation"] = fusa.Attestation{ + Status: fusa.StatusReviewed, + ImplementationAuthor: "auto", + IndependentReviewer: "Jane Doe ", + ReviewedAt: fusa.NowRFC3339(), + ContentHash: hash, + } + out, err := json.MarshalIndent(doc, "", " ") + if err != nil { + t.Fatalf("marshal %s: %v", path, err) + } + if err := os.WriteFile(path, out, 0o640); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} + +// injectReviewedAttestationSC is injectReviewedAttestation specialised for +// safety-case.json, whose content hash is computed over {nodes, edges} +// together (matching cmd_safetycase.go's gateContentQuality call), not a +// single top-level field. +func injectReviewedAttestationSC(t *testing.T, path string) { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + var doc map[string]interface{} + if unmarshalErr := json.Unmarshal(data, &doc); unmarshalErr != nil { + t.Fatalf("unmarshal %s: %v", path, unmarshalErr) + } + content := struct { + Nodes interface{} `json:"nodes"` + Edges interface{} `json:"edges"` + }{doc["nodes"], doc["edges"]} + hash, err := fusa.AttestationContentHash(content) + if err != nil { + t.Fatalf("AttestationContentHash: %v", err) + } + doc["attestation"] = fusa.Attestation{ + Status: fusa.StatusReviewed, + ImplementationAuthor: "auto", + IndependentReviewer: "Jane Doe ", + ReviewedAt: fusa.NowRFC3339(), + ContentHash: hash, + } + out, err := json.MarshalIndent(doc, "", " ") + if err != nil { + t.Fatalf("marshal %s: %v", path, err) + } + if err := os.WriteFile(path, out, 0o640); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} + +// assertAttestationPreserved fails the test unless path's current +// "attestation" field is present with status "reviewed" — i.e. the +// regenerating command carried it forward rather than discarding it. +func assertAttestationPreserved(t *testing.T, path string) { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + var doc struct { + Attestation *fusa.Attestation `json:"attestation"` + } + if err := json.Unmarshal(data, &doc); err != nil { + t.Fatalf("unmarshal %s: %v", path, err) + } + if doc.Attestation == nil { + t.Fatalf("%s: attestation was discarded on regeneration", path) + } + if doc.Attestation.Status != fusa.StatusReviewed { + t.Errorf("%s: attestation.status = %q, want %q", path, doc.Attestation.Status, fusa.StatusReviewed) + } + if doc.Attestation.IndependentReviewer != "Jane Doe " { + t.Errorf("%s: attestation.independentReviewer = %q, want the carried-forward reviewer", path, doc.Attestation.IndependentReviewer) + } +} diff --git a/cmd/gofusa/cmd_fmea.go b/cmd/gofusa/cmd_fmea.go index 56af01d..66fa0f0 100644 --- a/cmd/gofusa/cmd_fmea.go +++ b/cmd/gofusa/cmd_fmea.go @@ -88,8 +88,14 @@ func runFmea(args []string, stdout, stderr io.Writer) int { return fusa.ExitRuntime } - // Write fmea.json + // x-FuSa spec §1.6.2 MUST: carry forward any existing attestation from + // the prior saved fmea.json before overwriting it — a fresh fmea.Scan + // never has one of its own. Staleness (a content change since the + // review) falls out of gateContentQuality's own hash check below. jsonPath := filepath.Join(outDir, fmea.FMEAFile) + report.Attestation = carryForwardAttestation(jsonPath) + + // Write fmea.json if err := writeFmea(jsonPath, report, "json"); err != nil { fmt.Fprintf(stderr, "gofusa fmea: %v\n", err) return fusa.ExitRuntime diff --git a/cmd/gofusa/cmd_safetycase.go b/cmd/gofusa/cmd_safetycase.go index 047677f..893fab8 100644 --- a/cmd/gofusa/cmd_safetycase.go +++ b/cmd/gofusa/cmd_safetycase.go @@ -66,8 +66,15 @@ func runSafetyCase(args []string, stdout, stderr io.Writer) int { return fusa.ExitRuntime } - // Write safety-case.json + // x-FuSa spec §1.6.2 MUST: carry forward any existing attestation from + // the prior saved safety-case.json before overwriting it — a fresh + // safetycase.Build never has one of its own. Staleness (a content change + // since the review) falls out of gateContentQuality's own hash check + // below. jsonPath := filepath.Join(outDir, "safety-case.json") + sc.Attestation = carryForwardAttestation(jsonPath) + + // Write safety-case.json if err := writeFormatted(jsonPath, sc, "json"); err != nil { fmt.Fprintf(stderr, "gofusa safety-case: %v\n", err) return fusa.ExitRuntime diff --git a/cmd/gofusa/cmd_sas.go b/cmd/gofusa/cmd_sas.go index fd6d819..948325f 100644 --- a/cmd/gofusa/cmd_sas.go +++ b/cmd/gofusa/cmd_sas.go @@ -83,6 +83,20 @@ func runSas(args []string, stdout, stderr io.Writer) int { defer fmt.Fprintf(stdout, "SAS written to %s\n", outPath) } + // x-FuSa spec §1.6.2 MUST: carry forward any existing attestation from + // the prior saved sas.json (wherever this run's JSON output — primary or + // companion — will land) before overwriting it. A fresh sas.Build never + // has one of its own. Staleness (a content change since the review) + // falls out of gateContentQuality's own hash check below. Nothing is + // persisted (and so nothing to carry forward) when --output is "-". + if outPath != "" { + sasJSONPath := outPath + if *format != "json" { + sasJSONPath = filepath.Join(filepath.Dir(outPath), sas.SASJSONFile) + } + doc.Attestation = carryForwardAttestation(sasJSONPath) + } + if err := sas.Render(w, doc, *format); err != nil { fmt.Fprintf(stderr, "gofusa sas: render: %v\n", err) return fusa.ExitRuntime diff --git a/cmd/gofusa/cmd_tara.go b/cmd/gofusa/cmd_tara.go index 75305bb..ab08d6e 100644 --- a/cmd/gofusa/cmd_tara.go +++ b/cmd/gofusa/cmd_tara.go @@ -80,8 +80,14 @@ func runTara(args []string, stdout, stderr io.Writer) int { return fusa.ExitRuntime } - // Write tara.json + // x-FuSa spec §1.6.2 MUST: carry forward any existing attestation from + // the prior saved tara.json before overwriting it — a fresh tara.Scan + // never has one of its own. Staleness (a content change since the + // review) falls out of gateContentQuality's own hash check below. jsonPath := filepath.Join(outDir, tara.TARAFile) + report.Attestation = carryForwardAttestation(jsonPath) + + // Write tara.json if err := writeFile(jsonPath, func(f io.Writer) error { return tara.Render(f, report, "json") }); err != nil { diff --git a/cmd/gofusa/helpers.go b/cmd/gofusa/helpers.go index 48e6387..f529664 100644 --- a/cmd/gofusa/helpers.go +++ b/cmd/gofusa/helpers.go @@ -1,9 +1,11 @@ package main import ( + "encoding/json" "flag" "fmt" "io" + "os" fusa "github.com/SoundMatt/go-FuSa" "github.com/SoundMatt/go-FuSa/disposition" @@ -95,3 +97,33 @@ func gateContentQuality(stderr io.Writer, cmd, projectRoot, artifactFile string, } return code } + +// carryForwardAttestation loads path — the prior saved copy of the output +// file a command is about to rebuild/overwrite — and returns whatever +// §1.6.2 "attestation" object it carried, or nil if path is absent, +// unreadable, malformed, or has no attestation. x-FuSa spec §1.6.2 (MUST as +// of spec v1.15.0): before an artifact-producing command rebuilds its +// output, it MUST load any existing attestation from the prior saved output +// file and carry it forward onto the freshly-built result, rather than +// discarding it. Staleness then falls out automatically: a carried-forward +// contentHash that no longer matches the freshly-computed content hash +// means AttestationValid (via stubcheck.AttestationSuppresses) treats the +// attestation as not currently suppressing — never that it silently +// vanished. carryForwardAttestation only reads the "attestation" key, so it +// works uniformly across fmea.json/tara.json/safety-case.json/sas.json +// without needing each artifact's full schema. +// +//fusa:req REQ-CLI-HELPERS005 +func carryForwardAttestation(path string) *fusa.Attestation { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + var prior struct { + Attestation *fusa.Attestation `json:"attestation,omitempty"` + } + if err := json.Unmarshal(data, &prior); err != nil { + return nil + } + return prior.Attestation +} diff --git a/fusa.go b/fusa.go index 97d78a1..a968926 100644 --- a/fusa.go +++ b/fusa.go @@ -23,7 +23,7 @@ import ( ) // Version is the current release of go-FuSa. -const Version = "0.37.0" +const Version = "0.39.0" // SpecVersion is the x-FuSa spec version this release implements. const SpecVersion = "1.14.0" From eb0f1be69f67d183e1b449b99e2ccd734fd0cafb Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:46:30 -0700 Subject: [PATCH 2/3] docs: bump version references to 0.39.0 Signed-off-by: Matt Jones Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- README.md | 2 +- docs/tool-safety-manual.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9a6490..f48b48a 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ docker build -t go-fusa . docker run --rm -v "$(pwd)":/project go-fusa check ``` -Published tags: `latest`, `0.37`, `0.37.0` (and matching semver for every release). +Published tags: `latest`, `0.39`, `0.39.0` (and matching semver for every release). ## Standards coverage diff --git a/docs/tool-safety-manual.md b/docs/tool-safety-manual.md index 2123275..56902d2 100644 --- a/docs/tool-safety-manual.md +++ b/docs/tool-safety-manual.md @@ -1,6 +1,6 @@ # go-FuSa Tool Safety Manual -**Version:** 0.37.0 +**Version:** 0.39.0 **Module:** `github.com/SoundMatt/go-FuSa` **License:** Mozilla Public License 2.0 **Standards addressed:** ISO 26262, IEC 61508, ISO 21434, DO-178C From a51bce761f7b4c83b0b7f7f653fe73700bab600a Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:44:19 -0700 Subject: [PATCH 3/3] Merge remote-tracking branch 'origin/main' into fix/attestation-carry-forward-v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second round: main advanced to include #73 (hara, v0.42.0) since the previous merge. This branch's own release was auto-merged to a stale 0.42.0 (textually identical to what main now already ships) with no conflict — manually bumped it forward to v0.44.0 and reordered/deduped the CHANGELOG.md entries to reflect the actual chronology. Signed-off-by: Matt Jones Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- CHANGELOG.md | 52 +++++++++++++++++++------------------- README.md | 2 +- docs/tool-safety-manual.md | 2 +- fusa.go | 2 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92d9295..7324da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Dates reference the merged commit timestamp. ## [Unreleased] -## v0.42.0 — 2026-07-28 (x-FuSa spec §1.6.2 attestation carry-forward MUST) +## v0.44.0 — 2026-07-28 (x-FuSa spec §1.6.2 attestation carry-forward MUST) ### Fixed - **§1.6.2 attestation is no longer silently wiped on every regeneration** @@ -27,6 +27,31 @@ Dates reference the merged commit timestamp. as not currently suppressing (via `fusa.AttestationValid`), never that it vanished outright. +## v0.42.0 — 2026-07-28 (hara: risk.asil cross-validation + canonical standard id) + +### Fixed +- **HARA008: `risk.asil` is now cross-validated against `DetermineASIL(S,E,C)`** + (x-FuSa spec §1.2.5 MUST — ASIL determination). Previously a hazard's + stored `risk.asil` was accepted verbatim: `DetermineASIL` was only ever + used as a *fallback* for an empty value, so a hand-edited or + copy-pasted hazard could claim any ASIL regardless of its own S/E/C + inputs, with zero findings/gaps from either `gofusa hara show` or + `check`. The new `hara.ValidateASIL` (wrapped by the new engine rule + `HARA008`, and folded into `hara.Validate`'s own gap list so `hara show` + surfaces it directly) flags a hazard whose declared `risk.asil` disagrees + with the ISO 26262-3:2018 Table 4 value for its own severity/exposure/ + controllability — skipping hazards with an incomplete S/E/C rating + (HARA002's job) or no `risk.asil` set yet. +- **`standard` now uses the x-FuSa spec §2.4.1 canonical lowercase id** + (`iso26262`, not `"ISO 26262"`) in `.fusa-hara.json`: `hara init`'s + default `--standard` flag value changed from `"ISO 26262"` to + `"iso26262"`, the repo's own checked-in `.fusa-hara.json` was + normalised, and `hara.Load` now transparently normalises a legacy + display-string value (`"ISO 26262"`, `"IEC 61508"`, …) onto its + canonical id for backward compatibility with hand-authored files + predating this convention — an unrecognised id is still passed through + verbatim, never rejected. + ## v0.41.0 — 2026-07-28 (tara: closed impact/risk enums per x-FuSa spec v1.14.1) ### Fixed @@ -75,31 +100,6 @@ Dates reference the merged commit timestamp. continues to be suppressed only by a valid §1.6.2 attestation, never by disposition. -## v0.42.0 — 2026-07-28 (hara: risk.asil cross-validation + canonical standard id) - -### Fixed -- **HARA008: `risk.asil` is now cross-validated against `DetermineASIL(S,E,C)`** - (x-FuSa spec §1.2.5 MUST — ASIL determination). Previously a hazard's - stored `risk.asil` was accepted verbatim: `DetermineASIL` was only ever - used as a *fallback* for an empty value, so a hand-edited or - copy-pasted hazard could claim any ASIL regardless of its own S/E/C - inputs, with zero findings/gaps from either `gofusa hara show` or - `check`. The new `hara.ValidateASIL` (wrapped by the new engine rule - `HARA008`, and folded into `hara.Validate`'s own gap list so `hara show` - surfaces it directly) flags a hazard whose declared `risk.asil` disagrees - with the ISO 26262-3:2018 Table 4 value for its own severity/exposure/ - controllability — skipping hazards with an incomplete S/E/C rating - (HARA002's job) or no `risk.asil` set yet. -- **`standard` now uses the x-FuSa spec §2.4.1 canonical lowercase id** - (`iso26262`, not `"ISO 26262"`) in `.fusa-hara.json`: `hara init`'s - default `--standard` flag value changed from `"ISO 26262"` to - `"iso26262"`, the repo's own checked-in `.fusa-hara.json` was - normalised, and `hara.Load` now transparently normalises a legacy - display-string value (`"ISO 26262"`, `"IEC 61508"`, …) onto its - canonical id for backward compatibility with hand-authored files - predating this convention — an unrecognised id is still passed through - verbatim, never rejected. - ## v0.36.0 — 2026-07-28 (x-FuSa spec v1.13.0/v1.14.0 — evidence-artifact schema conformance + content-quality baseline) ### Added diff --git a/README.md b/README.md index 00b78f5..f0e7299 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ docker build -t go-fusa . docker run --rm -v "$(pwd)":/project go-fusa check ``` -Published tags: `latest`, `0.42`, `0.42.0` (and matching semver for every release). +Published tags: `latest`, `0.44`, `0.44.0` (and matching semver for every release). ## Standards coverage diff --git a/docs/tool-safety-manual.md b/docs/tool-safety-manual.md index c7a622e..fd4c3ff 100644 --- a/docs/tool-safety-manual.md +++ b/docs/tool-safety-manual.md @@ -1,6 +1,6 @@ # go-FuSa Tool Safety Manual -**Version:** 0.42.0 +**Version:** 0.44.0 **Module:** `github.com/SoundMatt/go-FuSa` **License:** Mozilla Public License 2.0 **Standards addressed:** ISO 26262, IEC 61508, ISO 21434, DO-178C diff --git a/fusa.go b/fusa.go index 2d928c4..c58364e 100644 --- a/fusa.go +++ b/fusa.go @@ -23,7 +23,7 @@ import ( ) // Version is the current release of go-FuSa. -const Version = "0.42.0" +const Version = "0.44.0" // SpecVersion is the x-FuSa spec version this release implements. const SpecVersion = "1.14.0"