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
815 changes: 815 additions & 0 deletions .fusa-reqs.json

Large diffs are not rendered by default.

49 changes: 48 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,54 @@ Dates reference the merged commit timestamp.

## [Unreleased]

## v0.33.5 — 2026-07-27
## v0.34.0 — 2026-07-27

### Fixed (issue #45)
- **`assess`/`sci`/`sas` builders reported false gaps for freshly-scaffolded
projects.** `gofusa init`/`gofusa template` default their output directory
to `docs/safety/`, but `iso26262.Assess`, `iec61508.Assess`, `do178.Assess`,
`sci.Build`, and `sas.Build` only ever checked for `SAFETY_PLAN.md`,
`SVP.md`, `SCMP.md`, and `SQAP.md` at the bare project root — the same
inconsistency `iec62443.go`'s `SECURITY.md`/`INCIDENT-RESPONSE.md` checks
had already been fixed for. Added `fusa.ResolveDoc`, a shared helper that
checks the project-root path first and falls back to `docs/safety/<name>`
for the known scaffold filenames, and wired it into all five builders.

### Added
- **`fusa.ResolveDoc(projectRoot, name string) string`** — new exported
helper (REQ-DOC001) resolving a scaffolded safety document's on-disk path,
used by the fix above.

### Requirement registry sync
- **Closed a 142-ID / 727-occurrence orphan-tag gap.** A repo-wide
`gofusa trace` audit found that entire subsystems — `sas/`, `sci/`,
`hara/`, `gapreport/`, `badge/`, `metrics/`, `disposition/`, `impact/`,
`analyze` rules ANA005-009, `coupling`, and ~40 `REQ-CLI-*`
CLI-subcommand tags — carried real `//fusa:req`/`//fusa:test`
annotations that were never registered in `.fusa-reqs.json`, so
`gofusa trace`'s own orphan-tag detection (TRACE009) was firing over
500 warnings against go-FuSa's own source. Registered all 142 requirement
IDs with title/text/standard derived from each implementation's doc
comment (or, where none existed, its CLI `--help` usage text), then
added the handful of missing test tags surfaced once those requirements
became visible to the coverage gate.
- Fixed a handful of ID inconsistencies uncovered along the way: five
test-side tags (`REQ-CLI-AUDIT001`, `REQ-CLI-AUDITPACK001`,
`REQ-CLI-BOUNDARY001`, `REQ-CLI-LINT001`, `REQ-CLI-VERSION001`)
duplicated already-registered IDs and were consolidated onto them;
three `gofusa trace --sec-tested` tests were retagged from
`REQ-CLI-TRACE003` (`--req-coverage`) to the correct `REQ-CLI-TRACE001`
(`--sec-tested`); `runCheck`/`runInit`/`runReport`/`runVerify` gained the
top-level `//fusa:req` tag their tests already assumed existed.
- Six synthetic fixture IDs (`REQ-001`, `REQ-002`, `REQ-003`, `REQ-H`,
`REQ-M`, `REQ-L`) used as sample data inside test-only Go source
fixtures were rewritten from raw string literals to string
concatenation, so they no longer get picked up by go-FuSa's own
self-trace of this repo.

### Requirements
- `.fusa-reqs.json`: 233 → 363 requirements.


### Fixed (issue #43)
- **Stale Dockerfile OCI labels** — the custom `io.x-fusa.spec-version` label
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ docker build -t go-fusa .
docker run --rm -v "$(pwd)":/project go-fusa check
```

Published tags: `latest`, `0.33`, `0.33.5` (and matching semver for every release).
Published tags: `latest`, `0.34`, `0.34.0` (and matching semver for every release).

## Standards coverage

Expand Down
1 change: 1 addition & 0 deletions cmd/gofusa/cmd_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/SoundMatt/go-FuSa/report"
)

//fusa:req REQ-CLI-CHECK001
func runCheck(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("gofusa check", flag.ContinueOnError)
fs.SetOutput(stderr)
Expand Down
1 change: 1 addition & 0 deletions cmd/gofusa/cmd_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/SoundMatt/go-FuSa/template"
)

//fusa:req REQ-CLI-INIT001
func runInit(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("gofusa init", flag.ContinueOnError)
fs.SetOutput(stderr)
Expand Down
1 change: 1 addition & 0 deletions cmd/gofusa/cmd_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/SoundMatt/go-FuSa/report"
)

//fusa:req REQ-CLI-REPORT001
func runReport(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("gofusa report", flag.ContinueOnError)
fs.SetOutput(stderr)
Expand Down
30 changes: 17 additions & 13 deletions cmd/gofusa/cmd_v023_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

// ─── Section 3a: runTraceSecTested ────────────────────────────────────────────

//fusa:test REQ-CLI-TRACE003
//fusa:test REQ-CLI-TRACE001
//fusa:test REQ-CLI-TRACE002
func TestRunTraceSecTested_Pass(t *testing.T) {
dir := t.TempDir()
// Write requirements file
Expand All @@ -23,12 +24,15 @@ func TestRunTraceSecTested_Pass(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, ".fusa-reqs.json"), []byte(reqs), 0o644); err != nil {
t.Fatal(err)
}
// Write Go files with //fusa:test tags for 3/4 requirements (75%)
src := `package main
//fusa:test REQ-001
//fusa:test REQ-002
//fusa:test REQ-003
`
// Write Go files with //fusa:test tags for 3/4 requirements (75%).
// Built via concatenation rather than a raw string literal so this
// fixture's own //fusa:test lines aren't picked up as real tags by
// go-FuSa's own self-trace of this repo (they're synthetic sample
// data scoped to dir, not real annotations on go-FuSa's own code).
src := "package main\n" +
"//fusa:test REQ-001\n" +
"//fusa:test REQ-002\n" +
"//fusa:test REQ-003\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand All @@ -43,7 +47,7 @@ func TestRunTraceSecTested_Pass(t *testing.T) {
}
}

//fusa:test REQ-CLI-TRACE003
//fusa:test REQ-CLI-TRACE001
func TestRunTraceSecTested_Fail(t *testing.T) {
dir := t.TempDir()
reqs := `{"requirements":[
Expand All @@ -55,10 +59,10 @@ func TestRunTraceSecTested_Fail(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, ".fusa-reqs.json"), []byte(reqs), 0o644); err != nil {
t.Fatal(err)
}
// Only 1 out of 4 has a test tag (25%)
src := `package main
//fusa:test REQ-001
`
// Only 1 out of 4 has a test tag (25%). Concatenated, not a raw
// string literal, for the same self-trace-noise reason as above.
src := "package main\n" +
"//fusa:test REQ-001\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand All @@ -70,7 +74,7 @@ func TestRunTraceSecTested_Fail(t *testing.T) {
}
}

//fusa:test REQ-CLI-TRACE003
//fusa:test REQ-CLI-TRACE001
func TestRunTraceSecTested_NoRequirements(t *testing.T) {
dir := t.TempDir()
var out, errBuf bytes.Buffer
Expand Down
2 changes: 2 additions & 0 deletions cmd/gofusa/cmd_v024_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func TestRunCapabilities_GeneratedAt(t *testing.T) {
// ─── helpers: usageErrorf / runtimeErrorf ─────────────────────────────────────

//fusa:test REQ-CLI-HELPERS001
//fusa:test REQ-CLI-HELPERS002
func TestUsageErrorf_ReturnsExitUsage(t *testing.T) {
var errBuf bytes.Buffer
code := usageErrorf(&errBuf, "mycommand", "bad value %q for flag %s", "xyz", "--foo")
Expand Down Expand Up @@ -170,6 +171,7 @@ func TestUsageErrorf_NoFormatArgs(t *testing.T) {
}

//fusa:test REQ-CLI-HELPERS001
//fusa:test REQ-CLI-HELPERS003
func TestRuntimeErrorf_ReturnsExitRuntime(t *testing.T) {
var errBuf bytes.Buffer
code := runtimeErrorf(&errBuf, "release", "failed to write SBOM: %v", "disk full")
Expand Down
44 changes: 24 additions & 20 deletions cmd/gofusa/cmd_v024b_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,15 @@ func TestRunReq_ShowAll(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, ".fusa-reqs.json"), []byte(reqs), 0o644); err != nil {
t.Fatal(err)
}
// Write a Go file with impl annotation
src := `package main
//fusa:req REQ-001
func Auth() {}
//fusa:test REQ-002
func TestSafety() {}
`
// Write a Go file with impl annotation. Concatenated, not a raw
// string literal, so this fixture's own //fusa:req/test lines
// aren't picked up as real tags by go-FuSa's own self-trace of
// this repo (they're synthetic sample data scoped to dir).
src := "package main\n" +
"//fusa:req REQ-001\n" +
"func Auth() {}\n" +
"//fusa:test REQ-002\n" +
"func TestSafety() {}\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand All @@ -255,11 +257,12 @@ func TestRunReq_ShowWithAnnotations(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, ".fusa-reqs.json"), []byte(reqs), 0o644); err != nil {
t.Fatal(err)
}
src := `package main
//fusa:req REQ-001
//fusa:test REQ-001
func Auth() {}
`
// Concatenated, not a raw string literal, for the same
// self-trace-noise reason as above.
src := "package main\n" +
"//fusa:req REQ-001\n" +
"//fusa:test REQ-001\n" +
"func Auth() {}\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -595,14 +598,15 @@ func TestRunFmea_BadFlag(t *testing.T) {
//fusa:test REQ-CLI013
func TestRunFmea_WithGoSource(t *testing.T) {
dir := t.TempDir()
src := `package main

//fusa:req REQ-001
func SafetyFunc() error { return nil }

//fusa:req REQ-002
func CriticalFunc() int { return 0 }
`
// Concatenated, not a raw string literal, for the same
// self-trace-noise reason as above.
src := "package main\n" +
"\n" +
"//fusa:req REQ-001\n" +
"func SafetyFunc() error { return nil }\n" +
"\n" +
"//fusa:req REQ-002\n" +
"func CriticalFunc() int { return 0 }\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand Down
27 changes: 15 additions & 12 deletions cmd/gofusa/cmd_v024c_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ func TestRunFmea_WithCyber(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n\ngo 1.22\n"), 0o644); err != nil {
t.Fatal(err)
}
src := `package main
//fusa:req REQ-001
func SafetyFunc() error { return nil }
`
// Concatenated, not a raw string literal, so this fixture's own
// //fusa:req line isn't picked up as a real tag by go-FuSa's own
// self-trace of this repo.
src := "package main\n" +
"//fusa:req REQ-001\n" +
"func SafetyFunc() error { return nil }\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -525,14 +527,15 @@ func TestReadModulePath_NoModule(t *testing.T) {
func TestCountBySeverity_AllLevels(t *testing.T) {
// Exercise countBySeverity with entries of different severities
dir := t.TempDir()
src := `package main
//fusa:req REQ-H
func High() {}
//fusa:req REQ-M
func Med() {}
//fusa:req REQ-L
func Low() {}
`
// Concatenated, not a raw string literal, for the same
// self-trace-noise reason as above.
src := "package main\n" +
"//fusa:req REQ-H\n" +
"func High() {}\n" +
"//fusa:req REQ-M\n" +
"func Med() {}\n" +
"//fusa:req REQ-L\n" +
"func Low() {}\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand Down
36 changes: 20 additions & 16 deletions cmd/gofusa/cmd_v024d_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -979,10 +979,12 @@ func transmit() string { return NoCrypto }
func TestRunFmea_CyberNoConfig(t *testing.T) {
// Test --cyber path when no .fusa.json present (uses default config)
dir := t.TempDir()
src := `package main
//fusa:req REQ-001
func DoWork() {}
`
// Concatenated, not a raw string literal, so this fixture's own
// //fusa:req line isn't picked up as a real tag by go-FuSa's own
// self-trace of this repo.
src := "package main\n" +
"//fusa:req REQ-001\n" +
"func DoWork() {}\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1023,7 +1025,7 @@ func TestRunDiff_BadFlag(t *testing.T) {

// ─── runAuditPack extra paths ─────────────────────────────────────────────────

//fusa:test REQ-CLI-AUDIT001
//fusa:test REQ-CLI016
func TestRunAuditPack_BadFlag(t *testing.T) {
var out, errBuf bytes.Buffer
code := runAuditPack([]string{"--no-such-flag"}, &out, &errBuf)
Expand All @@ -1049,17 +1051,19 @@ func TestRunSci_TextFormat(t *testing.T) {
func TestCountBySeverity_Direct(t *testing.T) {
// Run fmea on a project with real Go files to get actual entries
dir := t.TempDir()
src := `package main

//fusa:req REQ-H
func HighFunc() panic { panic("x") }

//fusa:req REQ-M
func MedFunc() error { return nil }

//fusa:req REQ-L
func LowFunc() {}
`
// Concatenated, not a raw string literal, so this fixture's own
// //fusa:req lines aren't picked up as real tags by go-FuSa's own
// self-trace of this repo.
src := "package main\n" +
"\n" +
"//fusa:req REQ-H\n" +
"func HighFunc() panic { panic(\"x\") }\n" +
"\n" +
"//fusa:req REQ-M\n" +
"func MedFunc() error { return nil }\n" +
"\n" +
"//fusa:req REQ-L\n" +
"func LowFunc() {}\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/gofusa/cmd_v024f_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ func TestRunCheck_WithOutputFilev2(t *testing.T) {

// ─── runAuditPack with output path ───────────────────────────────────────────

//fusa:test REQ-CLI-AUDITPACK001
//fusa:test REQ-CLI016
func TestRunAuditPack_WithOutputv2(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n\ngo 1.22\n"), 0o644); err != nil {
Expand All @@ -896,7 +896,7 @@ func TestRunAuditPack_WithOutputv2(t *testing.T) {

// ─── runBoundary JSON output ──────────────────────────────────────────────────

//fusa:test REQ-CLI-BOUNDARY001
//fusa:test REQ-CLI014
func TestRunBoundary_WithOutputDirv2(t *testing.T) {
dir := t.TempDir()
outDir := filepath.Join(dir, "bdry-out")
Expand All @@ -916,7 +916,7 @@ func TestRunBoundary_WithOutputDirv2(t *testing.T) {

// ─── runLint / runFiltered extra paths ───────────────────────────────────────

//fusa:test REQ-CLI-LINT001
//fusa:test REQ-CLI008
func TestRunLint_WithOutputFile(t *testing.T) {
dir := t.TempDir()
outFile := filepath.Join(dir, "lint.json")
Expand Down
6 changes: 3 additions & 3 deletions cmd/gofusa/cmd_v024h_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ func TestRunDiff_WithOutputFile(t *testing.T) {

// ─── runAuditPack dir error ───────────────────────────────────────────────────

//fusa:test REQ-CLI-AUDITPACK001
//fusa:test REQ-CLI016
func TestRunAuditPack_CreateError(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("root can write anywhere")
Expand Down Expand Up @@ -572,7 +572,7 @@ func TestRunMisra_BadFlagv2(t *testing.T) {

// ─── runVersion extra path ────────────────────────────────────────────────────

//fusa:test REQ-CLI-VERSION001
//fusa:test REQ-CLI004
func TestRunVersion_JSONFormat(t *testing.T) {
var out, errBuf bytes.Buffer
code := runVersion([]string{"--format", "json"}, &out, &errBuf)
Expand Down Expand Up @@ -646,7 +646,7 @@ func TestRunImpact_StaleArtifacts(t *testing.T) {

// ─── runLint output file and filtered paths ───────────────────────────────────

//fusa:test REQ-CLI-LINT001
//fusa:test REQ-CLI008
func TestRunLint_OutputFileError(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("root can write anywhere")
Expand Down
1 change: 1 addition & 0 deletions cmd/gofusa/cmd_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/SoundMatt/go-FuSa/verify"
)

//fusa:req REQ-CLI-VERIFY001
func runVerify(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("gofusa verify", flag.ContinueOnError)
fs.SetOutput(stderr)
Expand Down
Loading
Loading