diff --git a/.fusa-reqs.json b/.fusa-reqs.json index ec9044a..5152f13 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -1596,6 +1596,821 @@ "text": "trace.ScanTags shall walk all .go files under root (skipping vendor, testdata, and dot-prefixed directories), returning every //fusa:req, //fusa:test, and //fusa:sec-test annotation found as a Tag with its file, line, kind, and requirement ID.", "standard": "x-FuSa", "level": "1.4.1" + }, + { + "id": "REQ-DOC001", + "title": "ResolveDoc falls back to the docs/safety/ scaffold path", + "text": "fusa.ResolveDoc shall return the project-root-relative path to a scaffolded safety document if present, else the docs/safety/ path gofusa template/init write by default if that file is one of the known scaffold filenames and exists there instead, else an empty string, so Assess/Build evidence checks do not report false gaps for freshly-scaffolded projects.", + "standard": "ISO 26262", + "level": "ASIL-A" + }, + { + "id": "REQ-ANA005", + "title": "Dropped context detection", + "text": "The tool shall report an INFO finding (ANA005) when a function that already receives a context.Context parameter calls context.Background() or context.TODO() internally instead of propagating the received context.", + "standard": "ISO 26262", + "level": "ASIL-B" + }, + { + "id": "REQ-ANA006", + "title": "Unwrapped error detection", + "text": "The tool shall report a finding (ANA006) for fmt.Errorf calls that do not use the %w verb, since the resulting error loses its chain and callers cannot use errors.Is/As to inspect the underlying error.", + "standard": "ISO 26262", + "level": "ASIL-B" + }, + { + "id": "REQ-ANA007", + "title": "Unchecked two-result nil-dereference risk", + "text": "The tool shall report a finding (ANA007) when the non-error value from a two-result function call (val, err := f()) is used on the very next line without an intervening error check, indicating a nil-dereference risk.", + "standard": "ISO 26262", + "level": "ASIL-B" + }, + { + "id": "REQ-ANA008", + "title": "Goroutine shared-variable access", + "text": "The tool shall report a WARNING finding (ANA008) when a goroutine function literal accesses a package-level variable without going through a synchronisation primitive, indicating a potential data race.", + "standard": "ISO 26262", + "level": "ASIL-B" + }, + { + "id": "REQ-ANA009", + "title": "Dead code detection", + "text": "The tool shall report a finding (ANA009) for any statement that is unreachable because it follows an unconditional return, break, continue, or panic within the same block, per DO-178C \u00a76.4.4.2's prohibition on dead code at DAL-A/B.", + "standard": "ISO 26262", + "level": "ASIL-B" + }, + { + "id": "REQ-BADGE001", + "title": "New returns a Badge derived from finding counts", + "text": "New returns a Badge derived from finding counts.", + "standard": "x-FuSa" + }, + { + "id": "REQ-BADGE002", + "title": "Render writes the badge as a self-contained SVG to w", + "text": "Render writes the badge as a self-contained SVG to w.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-BADGE001", + "title": "Generate an SVG status badge", + "text": "Generate an SVG status badge. Reads a gofusa check --format json report, or reads from stdin if no file is given.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-CAP001", + "title": "Report the tool's supported commands, formats, and standards", + "text": "Report the tool's supported commands, formats, and standards.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-CHECK001", + "title": "Run safety checks", + "text": "Run safety checks. Exits 1 if any ERROR-severity findings exist.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-COUPLING001", + "title": "Analyse data and control coupling between packages (DO-178C \u00a76.4.4.3)", + "text": "Analyse data and control coupling between packages (DO-178C \u00a76.4.4.3). Writes coupling-report.json as evidence of coupling characterisation.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-COV001", + "title": "Produce a DO-178C structural coverage report from a Go coverage profile", + "text": "Produce a DO-178C structural coverage report from a Go coverage profile. Generate coverage.out with: go test -coverprofile=coverage.out ./...", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-DIFF001", + "title": "Compare two gofusa check --format json report files", + "text": "Compare two gofusa check --format json report files. Exits 1 if any new findings were introduced.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-DISP001", + "title": "Manage finding disposition entries", + "text": "Manage finding disposition entries. Subcommands: add Add a new disposition entry list List all disposition entries show Show the disposition entry for a specific rule Global flags: gofusa disposition: get working directory: %v", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-DO178-001", + "title": "Generate a DO-178C compliance gap report", + "text": "Generate a DO-178C compliance gap report. Maps evidence to Annex A objectives and reports PASS/GAP/MANUAL/N/A.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-FIX001", + "title": "Show auto-fixable findings from gofusa check", + "text": "Show auto-fixable findings from gofusa check. Prints a remediation summary; use --report to also save the report.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-HARA001", + "title": "Manage the Hazard Analysis and Risk Assessment (.fusa-hara.json)", + "text": "Manage the Hazard Analysis and Risk Assessment (.fusa-hara.json). Subcommands: show Display HARA as text or JSON (default) init Create a starter .fusa-hara.json asil Derive ASIL from S/E/C parameters Flags: gofusa hara: get working directory: %v", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-HELPERS001", + "title": "parseFlags parses a command's flag set and returns fusa.ExitUsage on error", + "text": "parseFlags parses a command's flag set and returns fusa.ExitUsage on error. Use in place of the inline `if err := fs.Parse(args); err != nil { return 1 }` pattern.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-HELPERS002", + "title": "usageErrorf prints a usage-error message to stderr and returns ExitUsage (2)", + "text": "usageErrorf prints a usage-error message to stderr and returns ExitUsage (2).", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-HELPERS003", + "title": "runtimeErrorf prints a runtime-error message to stderr and returns ExitRuntime (3)", + "text": "runtimeErrorf prints a runtime-error message to stderr and returns ExitRuntime (3).", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-HOOKS001", + "title": "Manage git hooks for go-FuSa integration", + "text": "Manage git hooks for go-FuSa integration. Subcommands: install Install pre-commit hook into .git/hooks/ remove Remove the go-FuSa pre-commit hook show Print the hook script to stdout", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-IEC61508-001", + "title": "Generate an IEC 61508 Parts 1-3 compliance gap report", + "text": "Generate an IEC 61508 Parts 1-3 compliance gap report.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-IMPACT001", + "title": "Analyse the impact of source changes on requirements and safety artefacts", + "text": "Analyse the impact of source changes on requirements and safety artefacts.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-INIT001", + "title": "Initialise a go-FuSa configuration in the project root", + "text": "Initialise a go-FuSa configuration in the project root.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-ISO21434-001", + "title": "Generate an ISO 21434 cybersecurity compliance gap report", + "text": "Generate an ISO 21434 cybersecurity compliance gap report.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-ISO26262-001", + "title": "Generate an ISO 26262 Part 6 compliance gap report", + "text": "Generate an ISO 26262 Part 6 compliance gap report.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-METRICS001", + "title": "Track go-FuSa safety metrics over time", + "text": "Track go-FuSa safety metrics over time. Subcommands: record Collect and append a metrics snapshot show Display the full metrics time series Global flags: gofusa metrics: get working directory: %v", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-MISRA001", + "title": "Show MISRA C:2023 to Go / go-FuSa rule coverage mapping", + "text": "Show MISRA C:2023 to Go / go-FuSa rule coverage mapping.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-PR001", + "title": "Manage software problem reports (DO-178C \u00a711.17)", + "text": "Manage software problem reports (DO-178C \u00a711.17). Subcommands: init Create an empty .fusa-problems.json add Add a new problem report list List all problem reports close Close a problem report", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-REPORT001", + "title": "Generate a safety compliance report", + "text": "Generate a safety compliance report. Always exits 0.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-REQ001", + "title": "Show, import, or export requirements", + "text": "Show, import, or export requirements. Subcommands: (none) Show requirements and their source/test locations import Import requirements from a CSV file export Export requirements to CSV", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-REQ002", + "title": "Import requirements from an external format", + "text": "gofusa req import --file [--format csv|doors|polarion|codebeamer|jama] shall import requirements from the given external-format file into the project's .fusa-reqs.json, exiting with a usage error if --file is not given.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-REQ003", + "title": "Export requirements to an external format", + "text": "gofusa req export [--format csv|doors|polarion|codebeamer|jama] [--output ] shall export the project's requirements to the given external format, writing to stdout by default.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-SAS001", + "title": "Generate a Software Accomplishment Summary (DO-178C \u00a711.20)", + "text": "Generate a Software Accomplishment Summary (DO-178C \u00a711.20).", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-SCI001", + "title": "Generate the Software Configuration Index (DO-178C \u00a711.16)", + "text": "Generate the Software Configuration Index (DO-178C \u00a711.16).", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-SIGN001", + "title": "Sign or verify a file using HMAC-SHA256", + "text": "Sign or verify a file using HMAC-SHA256. gofusa sign --key keyfile artifact.zip # creates artifact.zip.sig gofusa sign --verify --key keyfile artifact.zip # verifies artifact.zip.sig gofusa sign --keygen keyfile # generate a new key", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-TRACE001", + "title": "--sec-tested CLI gate", + "text": "gofusa trace --sec-tested N shall exit 1 when fewer than N%% of requirements have //fusa:test tags; N=0 disables the gate.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-TRACE002", + "title": "runTraceSecTested exits 1 if test coverage is below threshold percent", + "text": "runTraceSecTested exits 1 if test coverage is below threshold percent.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-TRACE003", + "title": "--req-coverage CLI gate", + "text": "gofusa trace --req-coverage N shall exit 1 when requirement coverage or function annotation density is below N%%; N=0 disables the gate.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-UNECE-001", + "title": "Generate a UN R.155 cybersecurity compliance gap report", + "text": "Generate a UN R.155 cybersecurity compliance gap report.", + "standard": "x-FuSa" + }, + { + "id": "REQ-CLI-VERIFY001", + "title": "Run go test and save a test evidence bundle", + "text": "Run go test and save a test evidence bundle.", + "standard": "x-FuSa" + }, + { + "id": "REQ-COMP001", + "title": "Complexity returns the cyclomatic complexity of a function body", + "text": "Complexity returns the cyclomatic complexity of a function body. V(G) = 1 + number of branching nodes (if, for, range, case, &&, ||, select case).", + "standard": "DO-178C", + "level": "6.3.4" + }, + { + "id": "REQ-COUP001", + "title": "Exported mutable package-level variable detection", + "text": "The tool shall report a finding (COUP001) for exported, non-const package-level variables, since they represent uncontrolled data coupling between packages per DO-178C \u00a76.4.4.3.", + "standard": "DO-178C", + "level": "6.4.4.3" + }, + { + "id": "REQ-COUP002", + "title": "Function/interface-parameter control coupling detection", + "text": "The tool shall report a finding (COUP002) when an exported function accepts a func or interface-typed parameter, since it represents control coupling with the caller per DO-178C \u00a76.4.4.3.", + "standard": "DO-178C", + "level": "6.4.4.3" + }, + { + "id": "REQ-COUPLING001", + "title": "NewDataCouplingRule returns a COUP001 rule instance for testing", + "text": "NewDataCouplingRule returns a COUP001 rule instance for testing.", + "standard": "DO-178C", + "level": "6.4.4.3" + }, + { + "id": "REQ-COUPLING002", + "title": "NewControlCouplingRule returns a COUP002 rule instance for testing", + "text": "NewControlCouplingRule returns a COUP002 rule instance for testing.", + "standard": "DO-178C", + "level": "6.4.4.3" + }, + { + "id": "REQ-COUPLING003", + "title": "CouplingReport is the persisted coupling analysis evidence artifact", + "text": "CouplingReport is the persisted coupling analysis evidence artifact. It satisfies DO-178C \u00a76.4.4.3 (data and control coupling characterisation).", + "standard": "DO-178C", + "level": "6.4.4.3" + }, + { + "id": "REQ-DIFF001", + "title": "Compare returns the delta between a baseline and a current report", + "text": "Compare returns the delta between a baseline and a current report.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DIFF002", + "title": "LoadReport reads and parses a JSON report file produced by gofusa check --format json", + "text": "LoadReport reads and parses a JSON report file produced by gofusa check --format json.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DIFF003", + "title": "Render writes the diff to w in the requested format (\"text\" or \"json\")", + "text": "Render writes the diff to w in the requested format (\"text\" or \"json\").", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP001", + "title": "Action describes what was decided for a finding", + "text": "Action describes what was decided for a finding.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP002", + "title": "Entry records a single disposition decision", + "text": "Entry records a single disposition decision.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP003", + "title": "Log is the full dispositions log for a project", + "text": "Log is the full dispositions log for a project.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP004", + "title": "Load reads the dispositions log from projectRoot", + "text": "Load reads the dispositions log from projectRoot. If the file does not exist, it returns an empty log with no error.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP005", + "title": "Save writes the dispositions log to path", + "text": "Save writes the dispositions log to path.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP006", + "title": "Add appends entry to log, deduplicating by RuleID+Action", + "text": "Add appends entry to log, deduplicating by RuleID+Action. If an entry with the same RuleID and Action already exists it is replaced. Returns the updated log.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP007", + "title": "IsDispositioned reports whether ruleID has any disposition entry", + "text": "IsDispositioned reports whether ruleID has any disposition entry.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP008", + "title": "Undispositioned ERROR finding detection", + "text": "The tool shall report an INFO finding (DISP001) for each ERROR finding present in check-report.json that has no corresponding disposition entry in .fusa-dispositions.json.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DISP009", + "title": "RenderEntries writes a human-readable table of entries to w", + "text": "RenderEntries writes a human-readable table of entries to w.", + "standard": "x-FuSa" + }, + { + "id": "REQ-DO178-001", + "title": "Objective is a single DO-178C compliance objective", + "text": "Objective is a single DO-178C compliance objective.", + "standard": "DO-178C" + }, + { + "id": "REQ-DO178-002", + "title": "Report is the complete DO-178C gap assessment", + "text": "Report is the complete DO-178C gap assessment.", + "standard": "DO-178C" + }, + { + "id": "REQ-DO178-003", + "title": "Render writes the gap report to w in the requested format (\"text\" or \"json\")", + "text": "Render writes the gap report to w in the requested format (\"text\" or \"json\").", + "standard": "DO-178C" + }, + { + "id": "REQ-GAPREPORT001", + "title": "Report is the canonical gap-report document (\u00a79.3 + \u00a73.1 common header)", + "text": "Report is the canonical gap-report document (\u00a79.3 + \u00a73.1 common header).", + "standard": "x-FuSa", + "level": "9.3" + }, + { + "id": "REQ-GAPREPORT002", + "title": "Render writes the report to w in the requested format (\"json\" or \"text\")", + "text": "Render writes the report to w in the requested format (\"json\" or \"text\").", + "standard": "x-FuSa", + "level": "9.3" + }, + { + "id": "REQ-GAPREPORT003", + "title": "New builds a canonical Report with header fields pre-populated", + "text": "New builds a canonical Report with header fields pre-populated.", + "standard": "x-FuSa", + "level": "9.3" + }, + { + "id": "REQ-GAPREPORT004", + "title": "AddObjective appends an objective and updates the summary counts", + "text": "AddObjective appends an objective and updates the summary counts.", + "standard": "x-FuSa", + "level": "9.3" + }, + { + "id": "REQ-GAPREPORT005", + "title": "Gap report text rendering", + "text": "gapreport.renderText shall write a human-readable rendering of a \u00a79.3 gap Report to the given writer, including the standard name, generation timestamp, project root, summary counts, and a status-icon-prefixed line per objective (with any associated finding rule IDs).", + "standard": "x-FuSa", + "level": "9.3" + }, + { + "id": "REQ-GAPREPORT006", + "title": "Gap report status icon mapping", + "text": "gapreport.statusIcon shall map each \u00a79.3 objective status string to its display icon used by the text renderer.", + "standard": "x-FuSa", + "level": "9.3" + }, + { + "id": "REQ-HARA001", + "title": "RiskRating holds the three ISO 26262-3 classification parameters and the derived ASIL", + "text": "RiskRating holds the three ISO 26262-3 classification parameters and the derived ASIL.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA002", + "title": "OperationalSituation describes a scenario in which a hazard can manifest", + "text": "OperationalSituation describes a scenario in which a hazard can manifest.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA003", + "title": "Hazard describes a potential source of harm", + "text": "Hazard describes a potential source of harm.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA004", + "title": "SafetyGoal is a top-level safety requirement derived from one or more hazards", + "text": "SafetyGoal is a top-level safety requirement derived from one or more hazards.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA005", + "title": "HARA is the full hazard analysis and risk assessment for a project", + "text": "HARA is the full hazard analysis and risk assessment for a project.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA006", + "title": "DetermineASIL derives the ASIL from S, E, C per ISO 26262-3:2018 Table 4", + "text": "DetermineASIL derives the ASIL from S, E, C per ISO 26262-3:2018 Table 4.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA007", + "title": "Load reads the HARA from projectRoot/.fusa-hara.json", + "text": "Load reads the HARA from projectRoot/.fusa-hara.json. Returns an empty HARA if the file does not exist.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA008", + "title": "Save writes the HARA to path", + "text": "Save writes the HARA to path.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA009", + "title": "Validate checks the HARA for completeness gaps", + "text": "Validate checks the HARA for completeness gaps.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA010", + "title": "Render writes the HARA to w in text, json, or markdown format", + "text": "Render writes the HARA to w in text, json, or markdown format.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA011", + "title": "HARA file presence check", + "text": "The tool shall report a finding (HARA001) when no .fusa-hara.json file is present in the project, since hazard analysis data is required for ISO 26262 projects.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA012", + "title": "Incomplete hazard risk rating detection", + "text": "The tool shall report a finding (HARA002) when a Hazard's risk rating is incomplete, i.e. Severity, Exposure, and Controllability are not all set.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA013", + "title": "Hazard with no linked safety goal detection", + "text": "The tool shall report a finding (HARA003) when a Hazard has no linked SafetyGoal, since every hazard must be mitigated by at least one safety goal.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA014", + "title": "Safety goal without ASIL detection", + "text": "The tool shall report a finding (HARA004) when a SafetyGoal has no ASIL assigned, since every safety goal must have an ASIL determined from its linked hazard.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HARA015", + "title": "Hazard ASIL exceeding project ASIL detection", + "text": "The tool shall report a finding (HARA005) when the highest hazard ASIL in the HARA exceeds the project ASIL declared in .fusa.json, since that means the project configuration understates risk.", + "standard": "ISO 26262" + }, + { + "id": "REQ-HTML004", + "title": "RenderEvidenceHTML writes a self-contained HTML evidence bundle to w", + "text": "RenderEvidenceHTML writes a self-contained HTML evidence bundle to w. It reads up to 16 evidence files from projectRoot and renders them as navigable sections. Missing files are shown as \"not present\" cards.", + "standard": "x-FuSa" + }, + { + "id": "REQ-IEC61508-001", + "title": "SIL represents a Safety Integrity Level", + "text": "SIL represents a Safety Integrity Level.", + "standard": "IEC 61508" + }, + { + "id": "REQ-IEC61508-002", + "title": "Objective is a single IEC 61508 compliance objective", + "text": "Objective is a single IEC 61508 compliance objective.", + "standard": "IEC 61508" + }, + { + "id": "REQ-IEC61508-003", + "title": "Report is the complete IEC 61508 gap assessment", + "text": "Report is the complete IEC 61508 gap assessment.", + "standard": "IEC 61508" + }, + { + "id": "REQ-IEC61508-004", + "title": "IEC 61508 gap report presence check", + "text": "The tool shall report an INFO finding (IEC61508001) when no iec61508-gap-report.json file is present, since the IEC 61508 gap report should be generated and committed to the repository.", + "standard": "IEC 61508" + }, + { + "id": "REQ-IMPACT001", + "title": "FileChange describes a single file that changed in the diff", + "text": "FileChange describes a single file that changed in the diff.", + "standard": "x-FuSa" + }, + { + "id": "REQ-IMPACT002", + "title": "RequirementImpact describes a requirement affected by changes", + "text": "RequirementImpact describes a requirement affected by changes.", + "standard": "x-FuSa" + }, + { + "id": "REQ-IMPACT003", + "title": "ArtifactStatus reports whether a safety artefact is stale", + "text": "ArtifactStatus reports whether a safety artefact is stale.", + "standard": "x-FuSa" + }, + { + "id": "REQ-IMPACT004", + "title": "Report is the complete impact analysis", + "text": "Report is the complete impact analysis.", + "standard": "x-FuSa" + }, + { + "id": "REQ-IMPACT005", + "title": "Render writes the impact report to w in the requested format (\"text\" or \"json\")", + "text": "Render writes the impact report to w in the requested format (\"text\" or \"json\").", + "standard": "x-FuSa" + }, + { + "id": "REQ-ISO26262-001", + "title": "ASIL represents an Automotive Safety Integrity Level", + "text": "ASIL represents an Automotive Safety Integrity Level.", + "standard": "ISO 26262" + }, + { + "id": "REQ-ISO26262-002", + "title": "Objective is a single ISO 26262 compliance objective", + "text": "Objective is a single ISO 26262 compliance objective.", + "standard": "ISO 26262" + }, + { + "id": "REQ-ISO26262-003", + "title": "Report is the complete ISO 26262 gap assessment", + "text": "Report is the complete ISO 26262 gap assessment.", + "standard": "ISO 26262" + }, + { + "id": "REQ-ISO26262-004", + "title": "ISO 26262 gap report presence check", + "text": "The tool shall report an INFO finding (ISO26262001) when no iso26262-gap-report.json file is present, since the ISO 26262 gap report should be generated and committed to the repository.", + "standard": "ISO 26262" + }, + { + "id": "REQ-ISO26262-005", + "title": "ASIL-less requirement detection", + "text": "The tool shall report a finding (ISO26262002) when requirements in .fusa-reqs.json lack ASIL annotations, since ISO 26262-6 \u00a76.4.2 requires ASIL designations on safety requirements.", + "standard": "ISO 26262" + }, + { + "id": "REQ-ISO26262-006", + "title": "Tool qualification failure detection", + "text": "The tool shall report a finding (ISO26262003) when qualify-report.json contains failed test cases, since a non-zero fail count undermines the evidence that the tool is fit for purpose.", + "standard": "ISO 26262" + }, + { + "id": "REQ-METRICS001", + "title": "Snapshot captures a point-in-time set of project safety metrics", + "text": "Snapshot captures a point-in-time set of project safety metrics.", + "standard": "x-FuSa" + }, + { + "id": "REQ-METRICS002", + "title": "TimeSeries is the full metrics history for a project", + "text": "TimeSeries is the full metrics history for a project.", + "standard": "x-FuSa" + }, + { + "id": "REQ-METRICS003", + "title": "Load reads the metrics time series from projectRoot", + "text": "Load reads the metrics time series from projectRoot. If the file does not exist it returns an empty series with no error.", + "standard": "x-FuSa" + }, + { + "id": "REQ-METRICS004", + "title": "Save writes ts to path", + "text": "Save writes ts to path.", + "standard": "x-FuSa" + }, + { + "id": "REQ-METRICS005", + "title": "Append adds snap to ts and returns the updated series", + "text": "Append adds snap to ts and returns the updated series.", + "standard": "x-FuSa" + }, + { + "id": "REQ-METRICS006", + "title": "Collect reads project artefacts and builds a metrics snapshot", + "text": "Collect reads project artefacts and builds a metrics snapshot. It reads check-report.json, .fusa-reqs.json, and coverage-report.json.", + "standard": "x-FuSa" + }, + { + "id": "REQ-METRICS007", + "title": "Render writes the time series to w in the requested format (\"text\" or \"json\")", + "text": "Render writes the time series to w in the requested format (\"text\" or \"json\").", + "standard": "x-FuSa" + }, + { + "id": "REQ-MISRA001", + "title": "Coverage describes how a MISRA rule is addressed", + "text": "Coverage describes how a MISRA rule is addressed.", + "standard": "MISRA C" + }, + { + "id": "REQ-MISRA002", + "title": "Rule is a single MISRA C:2023 rule and its Go coverage mapping", + "text": "Rule is a single MISRA C:2023 rule and its Go coverage mapping.", + "standard": "MISRA C" + }, + { + "id": "REQ-MISRA003", + "title": "Report is the full MISRA C:2023 coverage mapping", + "text": "Report is the full MISRA C:2023 coverage mapping.", + "standard": "MISRA C" + }, + { + "id": "REQ-MISRA004", + "title": "Assess builds the full MISRA C:2023 coverage report", + "text": "Assess builds the full MISRA C:2023 coverage report.", + "standard": "MISRA C" + }, + { + "id": "REQ-MISRA005", + "title": "Render writes the MISRA coverage report to w in the requested format (\"text\" or \"json\")", + "text": "Render writes the MISRA coverage report to w in the requested format (\"text\" or \"json\").", + "standard": "MISRA C" + }, + { + "id": "REQ-PR001", + "title": "ProblemReport is a single DO-178C problem report entry", + "text": "ProblemReport is a single DO-178C problem report entry.", + "standard": "DO-178C", + "level": "11.17" + }, + { + "id": "REQ-PR002", + "title": "Log is the complete problem report log", + "text": "Log is the complete problem report log.", + "standard": "DO-178C", + "level": "11.17" + }, + { + "id": "REQ-PR003", + "title": "Add appends a new problem report to the log and saves it", + "text": "Add appends a new problem report to the log and saves it.", + "standard": "DO-178C", + "level": "11.17" + }, + { + "id": "REQ-PR004", + "title": "Close marks a problem report as closed and saves it", + "text": "Close marks a problem report as closed and saves it.", + "standard": "DO-178C", + "level": "11.17" + }, + { + "id": "REQ-RELEASE009", + "title": "SPDX2xDocument is a Software Bill of Materials in SPDX 2.2 or 2.3 JSON format", + "text": "SPDX2xDocument is a Software Bill of Materials in SPDX 2.2 or 2.3 JSON format. Produced by ToSPDX22 and ToSPDX23; this format is more widely supported by older toolchains than SPDX 3.0.1 JSON-LD.", + "standard": "ISO 21434", + "level": "CAL-2" + }, + { + "id": "REQ-REPORT001", + "title": "Render writes r to w in the requested format (\"text\", \"json\", \"html\", \"sarif\", or \"md\")", + "text": "Render writes r to w in the requested format (\"text\", \"json\", \"html\", \"sarif\", or \"md\").", + "standard": "x-FuSa" + }, + { + "id": "REQ-REPORT002", + "title": "RenderToFile writes the report to path in format", + "text": "RenderToFile writes the report to path in format. If path is empty the report is written to stdout.", + "standard": "x-FuSa" + }, + { + "id": "REQ-RPT006", + "title": "SummaryTable holds per-category and per-rule finding breakdowns", + "text": "SummaryTable holds per-category and per-rule finding breakdowns.", + "standard": "x-FuSa" + }, + { + "id": "REQ-SARIF001", + "title": "SARIF output is valid JSON", + "text": "sarif.Render shall produce output that is valid JSON, so downstream tools (GitHub code scanning and compatible consumers) can parse it without error.", + "standard": "x-FuSa" + }, + { + "id": "REQ-SARIF002", + "title": "SARIF output declares schema version 2.1.0", + "text": "sarif.Render shall include a \"$schema\" field and declare \"version\": \"2.1.0\" in its output, per the SARIF 2.1.0 specification.", + "standard": "x-FuSa" + }, + { + "id": "REQ-SARIF003", + "title": "Report-level SARIF rendering", + "text": "report.renderSARIF shall render a Report's Findings as SARIF 2.1.0 using the current go-FuSa tool version, delegating to sarif.Render.", + "standard": "x-FuSa" + }, + { + "id": "REQ-SAS001", + "title": "EvidenceSummary describes a single piece of evidence in the SAS", + "text": "EvidenceSummary describes a single piece of evidence in the SAS.", + "standard": "DO-178C", + "level": "11.20" + }, + { + "id": "REQ-SAS002", + "title": "SAS is the Software Accomplishment Summary", + "text": "SAS is the Software Accomplishment Summary.", + "standard": "DO-178C", + "level": "11.20" + }, + { + "id": "REQ-SAS003", + "title": "Render writes the SAS in the requested format (\"markdown\" or \"json\") to w", + "text": "Render writes the SAS in the requested format (\"markdown\" or \"json\") to w.", + "standard": "DO-178C", + "level": "11.20" + }, + { + "id": "REQ-SCI001", + "title": "Item is a single software lifecycle data item", + "text": "Item is a single software lifecycle data item.", + "standard": "DO-178C", + "level": "11.16" + }, + { + "id": "REQ-SCI002", + "title": "SCI is the complete Software Configuration Index", + "text": "SCI is the complete Software Configuration Index.", + "standard": "DO-178C", + "level": "11.16" + }, + { + "id": "REQ-SCI003", + "title": "Render writes the SCI in the requested format (\"json\" or \"markdown\") to w", + "text": "Render writes the SCI in the requested format (\"json\" or \"markdown\") to w.", + "standard": "DO-178C", + "level": "11.16" + }, + { + "id": "REQ-TESTUTIL002", + "title": "MinimalProject returns a file map that satisfies all built-in FUSA rules", + "text": "MinimalProject returns a file map that satisfies all built-in FUSA rules.", + "standard": "x-FuSa" + }, + { + "id": "REQ-TESTUTIL003", + "title": "GoSource returns a map with a single Go source file at the given path", + "text": "GoSource returns a map with a single Go source file at the given path.", + "standard": "x-FuSa" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 63d29d6..f2efa03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/` + 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 diff --git a/README.md b/README.md index 486ea02..8c8a6bc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/gofusa/cmd_check.go b/cmd/gofusa/cmd_check.go index 737b04a..d8852ed 100644 --- a/cmd/gofusa/cmd_check.go +++ b/cmd/gofusa/cmd_check.go @@ -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) diff --git a/cmd/gofusa/cmd_init.go b/cmd/gofusa/cmd_init.go index e09174a..11b27f1 100644 --- a/cmd/gofusa/cmd_init.go +++ b/cmd/gofusa/cmd_init.go @@ -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) diff --git a/cmd/gofusa/cmd_report.go b/cmd/gofusa/cmd_report.go index 05eb03a..428d8fd 100644 --- a/cmd/gofusa/cmd_report.go +++ b/cmd/gofusa/cmd_report.go @@ -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) diff --git a/cmd/gofusa/cmd_v023_test.go b/cmd/gofusa/cmd_v023_test.go index 0bf6699..d371567 100644 --- a/cmd/gofusa/cmd_v023_test.go +++ b/cmd/gofusa/cmd_v023_test.go @@ -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 @@ -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) } @@ -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":[ @@ -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) } @@ -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 diff --git a/cmd/gofusa/cmd_v024_test.go b/cmd/gofusa/cmd_v024_test.go index d84118e..1f59219 100644 --- a/cmd/gofusa/cmd_v024_test.go +++ b/cmd/gofusa/cmd_v024_test.go @@ -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") @@ -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") diff --git a/cmd/gofusa/cmd_v024b_test.go b/cmd/gofusa/cmd_v024b_test.go index 7d22e7f..a6a0b6e 100644 --- a/cmd/gofusa/cmd_v024b_test.go +++ b/cmd/gofusa/cmd_v024b_test.go @@ -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) } @@ -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) } @@ -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) } diff --git a/cmd/gofusa/cmd_v024c_test.go b/cmd/gofusa/cmd_v024c_test.go index 80fa0cb..6863855 100644 --- a/cmd/gofusa/cmd_v024c_test.go +++ b/cmd/gofusa/cmd_v024c_test.go @@ -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) } @@ -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) } diff --git a/cmd/gofusa/cmd_v024d_test.go b/cmd/gofusa/cmd_v024d_test.go index 9bd4008..e8cfcc7 100644 --- a/cmd/gofusa/cmd_v024d_test.go +++ b/cmd/gofusa/cmd_v024d_test.go @@ -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) } @@ -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) @@ -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) } diff --git a/cmd/gofusa/cmd_v024f_test.go b/cmd/gofusa/cmd_v024f_test.go index c480624..f875454 100644 --- a/cmd/gofusa/cmd_v024f_test.go +++ b/cmd/gofusa/cmd_v024f_test.go @@ -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 { @@ -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") @@ -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") diff --git a/cmd/gofusa/cmd_v024h_test.go b/cmd/gofusa/cmd_v024h_test.go index a4bfdc7..6129bbf 100644 --- a/cmd/gofusa/cmd_v024h_test.go +++ b/cmd/gofusa/cmd_v024h_test.go @@ -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") @@ -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) @@ -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") diff --git a/cmd/gofusa/cmd_verify.go b/cmd/gofusa/cmd_verify.go index 524bd47..213e943 100644 --- a/cmd/gofusa/cmd_verify.go +++ b/cmd/gofusa/cmd_verify.go @@ -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) diff --git a/cmd/gofusa/main_test.go b/cmd/gofusa/main_test.go index bb05d54..7299c59 100644 --- a/cmd/gofusa/main_test.go +++ b/cmd/gofusa/main_test.go @@ -478,11 +478,13 @@ func TestRun_FMEA_GeneratesFiles(t *testing.T) { } func TestRun_FMEA_WithGoSource(t *testing.T) { - files := testutil.GoSource("mypkg/work.go", `package mypkg - -//fusa:req REQ-TEST001 -func DoWork() 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. + files := testutil.GoSource("mypkg/work.go", "package mypkg\n"+ + "\n"+ + "//fusa:req REQ-TEST001\n"+ + "func DoWork() error { return nil }\n") dir := testutil.ProjectDir(t, files) outDir := t.TempDir() var out, errOut bytes.Buffer diff --git a/disposition/disposition_test.go b/disposition/disposition_test.go index 343a34d..e12e199 100644 --- a/disposition/disposition_test.go +++ b/disposition/disposition_test.go @@ -42,6 +42,9 @@ func TestLoad_InvalidFile(t *testing.T) { } //fusa:test REQ-DISP004 +//fusa:test REQ-DISP001 +//fusa:test REQ-DISP002 +//fusa:test REQ-DISP003 func TestLoad_ValidFile(t *testing.T) { dir := t.TempDir() content := `{"project":"p","entries":[{"ruleID":"LINT001","rationale":"accepted","reviewer":"Alice","date":"2026-01-01T00:00:00Z","action":"accept"}]}` diff --git a/do178/do178.go b/do178/do178.go index 6c512ea..6e07776 100644 --- a/do178/do178.go +++ b/do178/do178.go @@ -19,6 +19,7 @@ import ( "strings" "time" + fusa "github.com/SoundMatt/go-FuSa" "github.com/SoundMatt/go-FuSa/gapreport" "github.com/SoundMatt/go-FuSa/trace" ) @@ -383,8 +384,7 @@ func Assess(projectRoot, project string, dal DAL) (*Report, error) { } // File-based evidence check - path := filepath.Join(projectRoot, filepath.FromSlash(o.file)) - if _, err := os.Stat(path); err == nil { + if path := fusa.ResolveDoc(projectRoot, o.file); path != "" { obj.Status = StatusPass obj.Evidence = o.file + " present" rep.Pass++ @@ -434,7 +434,7 @@ func checkSourceCode(projectRoot string, rep *Report) { plans := []string{"SAFETY_PLAN.md", "SVP.md", "SCMP.md", "SQAP.md"} present := 0 for _, p := range plans { - if _, err := os.Stat(filepath.Join(projectRoot, p)); err == nil { + if fusa.ResolveDoc(projectRoot, p) != "" { present++ } } diff --git a/do178/do178_test.go b/do178/do178_test.go index 8aa382f..a3ff8ea 100644 --- a/do178/do178_test.go +++ b/do178/do178_test.go @@ -50,6 +50,39 @@ func TestAssess_WithEvidence(t *testing.T) { } } +// Regression for #45: a freshly-scaffolded project (`gofusa template` +// defaults to docs/safety/) must not report the plan documents as gaps +// just because they aren't sitting at the project root, and the A-3.1 +// "all 4 plans present" manual-review check must also find them there. +// +//fusa:test REQ-DO178-001 +//fusa:test REQ-DOC001 +func TestAssess_DocsSafetyScaffoldPath(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + for _, f := range []string{"SAFETY_PLAN.md", "SVP.md", "SCMP.md", "SQAP.md"} { + if err := os.WriteFile(filepath.Join(dir, "docs", "safety", f), []byte("x"), 0o644); err != nil { + t.Fatalf("write %s: %v", f, err) + } + } + rep, err := do178.Assess(dir, "proj", do178.DALB) + if err != nil { + t.Fatalf("Assess: %v", err) + } + for _, obj := range rep.Objectives { + if obj.Gap != "" && (strings.Contains(obj.Gap, "SAFETY_PLAN.md") || + strings.Contains(obj.Gap, "SVP.md") || strings.Contains(obj.Gap, "SCMP.md") || + strings.Contains(obj.Gap, "SQAP.md")) { + t.Errorf("plan doc under docs/safety/ still reported as gap: %s", obj.Gap) + } + if obj.ID == "A-3.1" && obj.Status != do178.StatusManual { + t.Errorf("A-3.1 status = %v, want Manual (all 4 plans present under docs/safety/)", obj.Status) + } + } +} + //fusa:test REQ-DO178-001 func TestAssess_DALE_AllNA(t *testing.T) { dir := t.TempDir() diff --git a/docs/tool-safety-manual.md b/docs/tool-safety-manual.md index a078323..5cba36a 100644 --- a/docs/tool-safety-manual.md +++ b/docs/tool-safety-manual.md @@ -1,6 +1,6 @@ # go-FuSa Tool Safety Manual -**Version:** 0.33.5 +**Version:** 0.34.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/fmea/fmea_test.go b/fmea/fmea_test.go index ab8311b..fdafa2d 100644 --- a/fmea/fmea_test.go +++ b/fmea/fmea_test.go @@ -19,25 +19,28 @@ import ( // ─── fixtures ───────────────────────────────────────────────────────────────── -const srcExported = `package mypkg - -import "errors" - -// DoWork does some work. -// -//fusa:req REQ-TEST001 -func DoWork() error { - return errors.New("fail") -} - -// RunLoop starts a background loop. -func RunLoop() { - go func() {}() -} - -// helper is unexported and must be skipped. -func helper() {} -` +// 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 (it's synthetic sample source for exercising +// fmea's doc-comment extraction, not a real annotation). +const srcExported = "package mypkg\n" + + "\n" + + "import \"errors\"\n" + + "\n" + + "// DoWork does some work.\n" + + "//\n" + + "//fusa:req REQ-TEST001\n" + + "func DoWork() error {\n" + + "\treturn errors.New(\"fail\")\n" + + "}\n" + + "\n" + + "// RunLoop starts a background loop.\n" + + "func RunLoop() {\n" + + "\tgo func() {}()\n" + + "}\n" + + "\n" + + "// helper is unexported and must be skipped.\n" + + "func helper() {}\n" const srcNoExports = `package empty ` diff --git a/fusa.go b/fusa.go index aa1a9ea..6cd14d8 100644 --- a/fusa.go +++ b/fusa.go @@ -10,12 +10,14 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "os" + "path/filepath" "strings" "unicode" ) // Version is the current release of go-FuSa. -const Version = "0.33.5" +const Version = "0.34.0" // SpecVersion is the x-FuSa spec version this release implements. const SpecVersion = "1.10.12" @@ -186,3 +188,40 @@ func normalizeMessage(msg string) string { } return strings.TrimSpace(b.String()) } + +// docScaffoldFiles are the filenames `gofusa init`/`gofusa template` write +// into docs/safety/ by default (see template.Generate). Assess/report +// builders that check for these same files on disk also check +// docs/safety/ as a fallback to the bare project-root path, so a +// freshly-scaffolded project doesn't immediately report false gaps (#45). +var docScaffoldFiles = map[string]bool{ + "SAFETY_PLAN.md": true, + "HARA.md": true, + "TEST_EVIDENCE.md": true, + "SVP.md": true, + "SCMP.md": true, + "SQAP.md": true, + "IEC61508-FSP.md": true, + "ISO26262-FMEA.md": true, +} + +// ResolveDoc returns the on-disk path to name under projectRoot: the bare +// project-root-relative path if it exists there, otherwise +// docs/safety/ (gofusa's own scaffold default) if name is one of the +// files template.Generate writes and it exists there instead. It returns "" +// if neither location has the file. +// +//fusa:req REQ-DOC001 +func ResolveDoc(projectRoot, name string) string { + root := filepath.Join(projectRoot, filepath.FromSlash(name)) + if _, err := os.Stat(root); err == nil { + return root + } + if docScaffoldFiles[name] { + alt := filepath.Join(projectRoot, "docs", "safety", filepath.FromSlash(name)) + if _, err := os.Stat(alt); err == nil { + return alt + } + } + return "" +} diff --git a/fusa_test.go b/fusa_test.go index aecd7a8..3c41ffa 100644 --- a/fusa_test.go +++ b/fusa_test.go @@ -248,3 +248,62 @@ func TestComputeFingerprint_WhitespaceCollapsed(t *testing.T) { t.Error("messages with collapsed whitespace must yield the same fingerprint") } } + +//fusa:test REQ-DOC001 +func TestResolveDoc_RootTakesPriority(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatal(err) + } + root := filepath.Join(dir, "SAFETY_PLAN.md") + alt := filepath.Join(dir, "docs", "safety", "SAFETY_PLAN.md") + if err := os.WriteFile(root, []byte("root"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(alt, []byte("alt"), 0o644); err != nil { + t.Fatal(err) + } + if got := fusa.ResolveDoc(dir, "SAFETY_PLAN.md"); got != root { + t.Errorf("ResolveDoc = %q, want root path %q", got, root) + } +} + +//fusa:test REQ-DOC001 +func TestResolveDoc_FallsBackToDocsScaffoldPath(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatal(err) + } + alt := filepath.Join(dir, "docs", "safety", "SAFETY_PLAN.md") + if err := os.WriteFile(alt, []byte("alt"), 0o644); err != nil { + t.Fatal(err) + } + if got := fusa.ResolveDoc(dir, "SAFETY_PLAN.md"); got != alt { + t.Errorf("ResolveDoc = %q, want scaffold path %q", got, alt) + } +} + +//fusa:test REQ-DOC001 +func TestResolveDoc_NonScaffoldFileNoFallback(t *testing.T) { + // sbom.json is not one of gofusa's docs/safety/ scaffold files, so a + // docs/safety/sbom.json copy must NOT be treated as satisfying it — + // only the bare root path counts. + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "docs", "safety", "sbom.json"), []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + if got := fusa.ResolveDoc(dir, "sbom.json"); got != "" { + t.Errorf("ResolveDoc = %q, want \"\" (sbom.json has no docs/safety/ fallback)", got) + } +} + +//fusa:test REQ-DOC001 +func TestResolveDoc_NotFound(t *testing.T) { + dir := t.TempDir() + if got := fusa.ResolveDoc(dir, "SAFETY_PLAN.md"); got != "" { + t.Errorf("ResolveDoc = %q, want \"\"", got) + } +} diff --git a/gapreport/gapreport_test.go b/gapreport/gapreport_test.go index 48f8812..a6288d6 100644 --- a/gapreport/gapreport_test.go +++ b/gapreport/gapreport_test.go @@ -13,6 +13,7 @@ import ( // ─── New ────────────────────────────────────────────────────────────────────── //fusa:test REQ-GAPREPORT001 +//fusa:test REQ-GAPREPORT003 func TestNew_HeaderFields(t *testing.T) { r := gapreport.New("/some/project", "iso26262") if r.SchemaVersion == "" { @@ -55,6 +56,7 @@ func TestNew_EmptyObjectives(t *testing.T) { // ─── AddObjective / Summary ─────────────────────────────────────────────────── //fusa:test REQ-GAPREPORT001 +//fusa:test REQ-GAPREPORT004 func TestAddObjective_Satisfied(t *testing.T) { r := gapreport.New(".", "iso26262") r.AddObjective(gapreport.Objective{ @@ -285,6 +287,8 @@ func TestRender_Text_HeaderAndSummary(t *testing.T) { } //fusa:test REQ-GAPREPORT002 +//fusa:test REQ-GAPREPORT005 +//fusa:test REQ-GAPREPORT006 func TestRender_Text_StatusIcons(t *testing.T) { r := gapreport.New(".", "iec61508") r.AddObjective(gapreport.Objective{ID: "A", Status: gapreport.StatusSatisfied, Title: "Sat"}) diff --git a/hara/hara_test.go b/hara/hara_test.go index bbcc0dd..c68bec2 100644 --- a/hara/hara_test.go +++ b/hara/hara_test.go @@ -103,6 +103,11 @@ func TestLoad_InvalidJSON(t *testing.T) { } //fusa:test REQ-HARA008 +//fusa:test REQ-HARA001 +//fusa:test REQ-HARA002 +//fusa:test REQ-HARA003 +//fusa:test REQ-HARA004 +//fusa:test REQ-HARA005 func TestSave_RoundTrip(t *testing.T) { dir := t.TempDir() h := &hara.HARA{ diff --git a/iec61508/iec61508.go b/iec61508/iec61508.go index ff94e87..d8fd6cc 100644 --- a/iec61508/iec61508.go +++ b/iec61508/iec61508.go @@ -290,8 +290,7 @@ func Assess(projectRoot, project string, sil SIL) (*Report, error) { } // File-based evidence check - path := filepath.Join(projectRoot, filepath.FromSlash(o.file)) - if _, err := os.Stat(path); err == nil { + if path := fusa.ResolveDoc(projectRoot, o.file); path != "" { obj.Status = StatusPass obj.Evidence = o.file + " present" rep.Pass++ diff --git a/iec61508/iec61508_test.go b/iec61508/iec61508_test.go index 6a7dd70..344025a 100644 --- a/iec61508/iec61508_test.go +++ b/iec61508/iec61508_test.go @@ -50,6 +50,31 @@ func TestAssess_WithEvidence(t *testing.T) { } } +// Regression for #45: a freshly-scaffolded project (`gofusa template` +// defaults to docs/safety/) must not report SAFETY_PLAN.md as a gap just +// because it isn't sitting at the project root. +// +//fusa:test REQ-IEC61508-001 +//fusa:test REQ-DOC001 +func TestAssess_DocsSafetyScaffoldPath(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "docs", "safety", "SAFETY_PLAN.md"), []byte("x"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + rep, err := iec61508.Assess(dir, "proj", iec61508.SIL2) + if err != nil { + t.Fatalf("Assess: %v", err) + } + for _, obj := range rep.Objectives { + if obj.Gap != "" && strings.Contains(obj.Gap, "SAFETY_PLAN.md") { + t.Errorf("SAFETY_PLAN.md under docs/safety/ still reported as gap: %s", obj.Gap) + } + } +} + //fusa:test REQ-IEC61508-001 func TestAssess_SIL4_MCDC_Manual(t *testing.T) { dir := t.TempDir() diff --git a/impact/impact_test.go b/impact/impact_test.go index 9b163f8..a474a65 100644 --- a/impact/impact_test.go +++ b/impact/impact_test.go @@ -124,6 +124,10 @@ func TestAnalyse_FromRef(t *testing.T) { _ = rep // may or may not have changes depending on git version } +//fusa:test REQ-IMPACT001 +//fusa:test REQ-IMPACT002 +//fusa:test REQ-IMPACT004 +//fusa:test REQ-IMPACT005 func TestRender_Text(t *testing.T) { rep := &impact.Report{ ChangedFiles: []impact.FileChange{ @@ -171,6 +175,7 @@ func TestRender_InvalidFormat(t *testing.T) { } } +//fusa:test REQ-IMPACT003 func TestRenderText_StaleAndRerun(t *testing.T) { rep := &impact.Report{ ChangedFiles: []impact.FileChange{ diff --git a/iso26262/iso26262.go b/iso26262/iso26262.go index 3f37e4d..aea7dcb 100644 --- a/iso26262/iso26262.go +++ b/iso26262/iso26262.go @@ -263,8 +263,7 @@ func Assess(projectRoot, project string, asil ASIL) (*Report, error) { } // File-based evidence check - path := filepath.Join(projectRoot, filepath.FromSlash(o.file)) - if _, err := os.Stat(path); err == nil { + if path := fusa.ResolveDoc(projectRoot, o.file); path != "" { obj.Status = StatusPass obj.Evidence = o.file + " present" rep.Pass++ diff --git a/iso26262/iso26262_test.go b/iso26262/iso26262_test.go index 58fef72..13c512b 100644 --- a/iso26262/iso26262_test.go +++ b/iso26262/iso26262_test.go @@ -51,6 +51,31 @@ func TestAssess_WithEvidence(t *testing.T) { } } +// Regression for #45: a freshly-scaffolded project (`gofusa template` +// defaults to docs/safety/) must not report SAFETY_PLAN.md as a gap just +// because it isn't sitting at the project root. +// +//fusa:test REQ-ISO26262-001 +//fusa:test REQ-DOC001 +func TestAssess_DocsSafetyScaffoldPath(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "docs", "safety", "SAFETY_PLAN.md"), []byte("x"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + rep, err := iso26262.Assess(dir, "proj", iso26262.ASILB) + if err != nil { + t.Fatalf("Assess: %v", err) + } + for _, obj := range rep.Objectives { + if obj.Gap != "" && strings.Contains(obj.Gap, "SAFETY_PLAN.md") { + t.Errorf("SAFETY_PLAN.md under docs/safety/ still reported as gap: %s", obj.Gap) + } + } +} + //fusa:test REQ-ISO26262-001 func TestAssess_ASILC_SafetyCase(t *testing.T) { dir := t.TempDir() diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go index 5eafb2c..7e4c8eb 100644 --- a/metrics/metrics_test.go +++ b/metrics/metrics_test.go @@ -39,6 +39,8 @@ func TestLoad_InvalidFile(t *testing.T) { } //fusa:test REQ-METRICS004 +//fusa:test REQ-METRICS001 +//fusa:test REQ-METRICS002 func TestSave_RoundTrip(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, metrics.MetricsFile) diff --git a/misra/misra_test.go b/misra/misra_test.go index 5bf0077..7ef2a29 100644 --- a/misra/misra_test.go +++ b/misra/misra_test.go @@ -73,6 +73,7 @@ func TestAssess_SpecificRules(t *testing.T) { } //fusa:test REQ-MISRA004 +//fusa:test REQ-MISRA005 func TestRender_Text(t *testing.T) { rep := misra.Assess() var buf bytes.Buffer diff --git a/release/release_test.go b/release/release_test.go index 0d4e0ee..3c9937f 100644 --- a/release/release_test.go +++ b/release/release_test.go @@ -418,6 +418,7 @@ func TestToSPDX23_VersionString(t *testing.T) { } } +//fusa:test REQ-RELEASE009 func TestToSPDX22_RequiredFields(t *testing.T) { sbom := buildSBOMForSPDX(t) doc := release.ToSPDX22(sbom) diff --git a/report/report_test.go b/report/report_test.go index e212f10..2d69734 100644 --- a/report/report_test.go +++ b/report/report_test.go @@ -63,6 +63,7 @@ func TestNew_EmptyFindings(t *testing.T) { } //fusa:test REQ-RPT001 +//fusa:test REQ-REPORT001 func TestRender_Text_ContainsFindings(t *testing.T) { r := report.New("/proj", testFindings) var buf bytes.Buffer @@ -177,6 +178,7 @@ func TestRender_UnknownFormat(t *testing.T) { } } +//fusa:test REQ-REPORT002 func TestRenderToFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "report.txt") @@ -224,6 +226,7 @@ func TestRender_SARIF_Valid(t *testing.T) { // ─── html_bundle ────────────────────────────────────────────────────────────── +//fusa:test REQ-HTML004 func TestRenderEvidenceHTML_EmptyDir(t *testing.T) { dir := t.TempDir() var buf bytes.Buffer diff --git a/sas/sas.go b/sas/sas.go index 2ca6ea1..41da5ac 100644 --- a/sas/sas.go +++ b/sas/sas.go @@ -13,9 +13,9 @@ import ( "encoding/json" "fmt" "io" - "os" - "path/filepath" "time" + + fusa "github.com/SoundMatt/go-FuSa" ) // SASFile is the default output filename. @@ -93,13 +93,12 @@ func Build(projectRoot, project, version, dal, prepared string) (*SAS, error) { var gaps []string for _, item := range evidenceItems { - path := filepath.Join(projectRoot, filepath.FromSlash(item.file)) ev := EvidenceSummary{ Title: item.title, File: item.file, Summary: item.desc, } - if _, err := os.Stat(path); err == nil { + if fusa.ResolveDoc(projectRoot, item.file) != "" { ev.Present = true } else { gaps = append(gaps, fmt.Sprintf("%s (%s) — not found", item.title, item.file)) diff --git a/sas/sas_test.go b/sas/sas_test.go index ecdb676..32a4c34 100644 --- a/sas/sas_test.go +++ b/sas/sas_test.go @@ -70,6 +70,34 @@ func TestBuild_AllPresent(t *testing.T) { } } +// Regression for #45: a freshly-scaffolded project (`gofusa template` +// defaults to docs/safety/) must not report the plan documents as gaps +// just because they aren't sitting at the project root. +// +//fusa:test REQ-SAS001 +//fusa:test REQ-DOC001 +func TestBuild_DocsSafetyScaffoldPath(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + for _, f := range []string{"SAFETY_PLAN.md", "SVP.md", "SCMP.md", "SQAP.md"} { + if err := os.WriteFile(filepath.Join(dir, "docs", "safety", f), []byte("x"), 0o644); err != nil { + t.Fatalf("write %s: %v", f, err) + } + } + doc, err := sas.Build(dir, "proj", "1.0.0", "DAL-B", "Test Team") + if err != nil { + t.Fatalf("Build: %v", err) + } + for _, g := range doc.Gaps { + if strings.Contains(g, "SAFETY_PLAN.md") || strings.Contains(g, "SVP.md") || + strings.Contains(g, "SCMP.md") || strings.Contains(g, "SQAP.md") { + t.Errorf("plan doc under docs/safety/ still reported as gap: %s", g) + } + } +} + //fusa:test REQ-SAS003 func TestRender_Markdown(t *testing.T) { dir := t.TempDir() diff --git a/sci/sci.go b/sci/sci.go index 9fbb0f0..228946d 100644 --- a/sci/sci.go +++ b/sci/sci.go @@ -15,6 +15,8 @@ import ( "os" "path/filepath" "time" + + fusa "github.com/SoundMatt/go-FuSa" ) // SCIFile is the default output filename. @@ -99,13 +101,16 @@ func Build(projectRoot, project, version string) (*SCI, error) { Generated: time.Now().UTC(), } for _, c := range catalog { - path := filepath.Join(projectRoot, filepath.FromSlash(c.file)) item := Item{ Name: c.name, File: c.file, Class: c.class, Note: c.note, } + path := fusa.ResolveDoc(projectRoot, c.file) + if path == "" { + path = filepath.Join(projectRoot, filepath.FromSlash(c.file)) + } if data, err := os.ReadFile(path); err == nil { item.Present = true h := sha256.Sum256(data) diff --git a/sci/sci_test.go b/sci/sci_test.go index 7e2c655..871fafe 100644 --- a/sci/sci_test.go +++ b/sci/sci_test.go @@ -62,6 +62,36 @@ func TestBuild_WithSomeFiles(t *testing.T) { } } +// Regression for #45: a freshly-scaffolded project (`gofusa template` +// defaults to docs/safety/) must not report SAFETY_PLAN.md as absent from +// the SCI just because it isn't sitting at the project root. +// +//fusa:test REQ-SCI001 +//fusa:test REQ-DOC001 +func TestBuild_DocsSafetyScaffoldPath(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "docs", "safety"), 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "docs", "safety", "SAFETY_PLAN.md"), []byte("x"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + index, err := sci.Build(dir, "proj", "1.0.0") + if err != nil { + t.Fatalf("Build: %v", err) + } + for _, it := range index.Items { + if it.File == "SAFETY_PLAN.md" { + if !it.Present { + t.Error("SAFETY_PLAN.md under docs/safety/ should be Present") + } + if it.SHA256 == "" { + t.Error("SAFETY_PLAN.md under docs/safety/ should have a SHA256") + } + } + } +} + //fusa:test REQ-SCI001 func TestBuild_SHA256Stable(t *testing.T) { dir := t.TempDir() diff --git a/testutil/testutil_test.go b/testutil/testutil_test.go index aec040e..1386fad 100644 --- a/testutil/testutil_test.go +++ b/testutil/testutil_test.go @@ -46,6 +46,7 @@ func TestProjectDir_CleanedUp(t *testing.T) { } } +//fusa:test REQ-TESTUTIL002 func TestMinimalProject_ContainsRequiredFiles(t *testing.T) { files := testutil.MinimalProject() required := []string{ @@ -76,6 +77,7 @@ func TestMinimalProject_ConfigIsValidJSON(t *testing.T) { } } +//fusa:test REQ-TESTUTIL003 func TestGoSource_IncludesMinimalProject(t *testing.T) { const relPath = "myfile.go" const src = "package mypkg\n\nfunc Foo() {}\n"