diff --git a/docs/config.md b/docs/config.md index 9bba9ac..e1c954b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -334,6 +334,10 @@ declaration is accepted by the consumer's checker; it does not establish that each fragment conforms to a complete application schema. Final assembled decoding and application validation still run once. +A header that is absent below a structural control follows the same omission +policy. Actual header/control overlap is rejected, and ordinary structural +validation and main-version ownership protection continue independently. + Accepted non-main declarations are consumed from their owned payload before composition; their authored ancestors remain, even if empty. The main declaration remains in the application. Ordinary release metadata should use a diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index f207977..22b2bac 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -707,3 +707,14 @@ classification. Focused source-version/sensitivity/history tests, `GOTOOLCHAIN=local GOPROXY=off go test -race ./...` passed on `go1.26.9 linux/amd64`. Existing historical/dependency propagation and neighboring public-value coverage remain green; no new sensitivity policy was introduced. + +Additional remediation regressions protect import opt-in +(`TestFacadeSourceVersionModesPreserveImportOptIn`), complete absence paths +(`TestSourceVersionAbsentPathKeepsUnresolvedSegmentIntent` and +`TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath`), and optional missing +headers below controls +(`TestFacadeSourceVersionMissingHeaderDoesNotOverlapControl`). Each failed +before its production correction and passes afterward. All four independent +review probes, the final everyday check, and the full race suite now pass on +`go1.26.9 linux/amd64`. The remediation record contains completion evidence; +the agreed source-version policy and release gates are unchanged. diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 23c91aa..99258bb 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -158,7 +158,8 @@ state. Main's local header is retained, accepted non-main headers are removed before placement/composition, and the destination-prefixed main rule reaches payload protection and typed conversion. Warnings, raw accounting, source contexts, and final loading reuse the existing pipeline. Four must-address -issues remain. Findings 1 and 2 above remain completed historical findings. +issues were identified and are now resolved below. Findings 1 and 2 above remain +completed historical findings. ### 3. Preserve distinct semantic shapes during evidence classification (P1) @@ -304,7 +305,15 @@ Required remediation: ### 6. Apply structural-overlap rejection only to an actual declaration (P2) -Status: open. This is an unintended strengthening of the agreed restriction. +Status: complete. Overlap rejection now requires an actually present selected +declaration. Optional absence proceeds without a non-main checker invocation; +required absence and malformed controls still fail under their existing rules. +The public regression failed before the fix and now covers valid inheritance +in separate mounted scopes, required omission with NoSource main, and invalid +inheritance controls. Existing present-overlap tests remain green. Focused +source-version/directive/inheritance tests, the everyday check, and the full +race suite passed on `go1.26.9 linux/amd64`. The configuration contract clarifies +the omission boundary. This removes the unintended strengthened restriction. Affected: `processSourceVersion` in [source_version.go](../../internal/engine/source_version.go), lines 197–219 at @@ -368,3 +377,16 @@ temporary directory. No production fixes, repository regression tests, dependency updates, or release changes were made. Only this findings document was revised. + +### Remediation completion validation + +All four external review probes now pass against the corrected checkout, +including both shape-collision acquisition orders. The final everyday check and +`GOTOOLCHAIN=local GOPROXY=off go test -race ./...` passed on +`go1.26.9 linux/amd64`. Each new regression was observed failing before its +corresponding production fix. + +The earlier completed Mapping diagnostic regression was rerun successfully, and +ADR 0011 from `## Date` onward still matches its accepted text at `7248a8b`. +No findings remain open. No dependencies, source-version policies, or release +gates changed, and no release was published. diff --git a/facade_source_version_mode_test.go b/facade_source_version_mode_test.go index b6c2f36..1e8c9a0 100644 --- a/facade_source_version_mode_test.go +++ b/facade_source_version_mode_test.go @@ -170,6 +170,68 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) { } } +func TestFacadeSourceVersionMissingHeaderDoesNotOverlapControl(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + mode configkit.SourceVersionMode + control string + wantCode configkit.Code + }{ + {"optional omission", configkit.SourceVersionModeIfPresent, "base", ""}, + {"required omission", configkit.SourceVersionModeRequired, "base", configkit.CodeVersionError}, + {"invalid control", configkit.SourceVersionModeIfPresent, "true", configkit.CodeInheritanceInvalid}, + } { + t.Run(test.name, func(t *testing.T) { + main := configkit.Bytes("main", []byte("{}\n")) + if test.mode == configkit.SourceVersionModeRequired { + main = configkit.NoSource() + } + checks := 0 + path := configkit.Keys("entries", "child", "extends", "version") + loaded, err := configkit.LoadLayered(configkit.Input{Source: main, Destination: configkit.Keys("main")}, configkit.LayeredOptions[map[string]any]{ + SourceVersionMode: test.mode, + Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{ + Path: path, Check: func(version configkit.Version, _ *configkit.Validation) error { + checks++ + if version.Present { + t.Error("checker received a nonexistent declaration") + } + return nil + }, + }}, + Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{ + Source: configkit.Bytes("underlay", []byte("entries:\n base: {value: good}\n child: {extends: "+test.control+"}\n")), Destination: configkit.Keys("under"), + }}}}, + }) + if checks != 1 { + t.Errorf("checker ran %d times, want absent main only", checks) + } + if test.wantCode != "" { + var failure *configkit.LoadError + if loaded != nil || !errors.As(err, &failure) { + t.Fatal("invalid input did not fail safely") + } + if diagnostics := failure.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Code != test.wantCode { + t.Fatalf("diagnostics=%#v", diagnostics) + } + return + } + if err != nil { + t.Fatal(err) + } + child := loaded.Config["under"].(map[string]any)["entries"].(map[string]any)["child"].(map[string]any) + if child["value"] != "good" { + t.Fatal("valid sibling inheritance was not applied") + } + check := loaded.Sources()[1].VersionCheck + if check == nil || check.Version.Present || check.Path.String() != path.String() { + t.Fatal("optional omission evidence was not retained") + } + }) + } +} + func (reader *sourceVersionModeReader) Read(data []byte) (int, error) { reader.reads++ return reader.Reader.Read(data) diff --git a/internal/engine/source_version.go b/internal/engine/source_version.go index befad29..5ecaa2a 100644 --- a/internal/engine/source_version.go +++ b/internal/engine/source_version.go @@ -196,7 +196,7 @@ func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVer interpreted := interpretVersion(rule, root) evidence.version = interpreted.version - if sourceVersionControlOverlap(root, evidence.resolvedPath) { + if resolved.present && sourceVersionControlOverlap(root, evidence.resolvedPath) { evidence.redactText() result.payload = nil result.diagnostics = []Diagnostic{sourceVersionOverlapDiagnostic(rule, evidence)}