Separate version interpretation from reporting

This commit is contained in:
2026-10-15 15:04:57 +00:00
parent 8645cab79a
commit 2fcf892324
3 changed files with 84 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
# Implement shared source version checks # Implement shared source version checks
Status: Stage 1 complete; runtime implementation has not started. Status: Stages 1–2 complete; source-local extraction has not started.
Audience: a Luna coding agent implementing one numbered stage per prompt, in Audience: a Luna coding agent implementing one numbered stage per prompt, in
order. The [feature roadmap](source-version-checks.md) owns target behavior and order. The [feature roadmap](source-version-checks.md) owns target behavior and
@@ -241,6 +241,20 @@ were made.
Validate: engine version/validation and facade version tests; everyday check. Validate: engine version/validation and facade version tests; everyday check.
Done: shared mechanisms exist with unchanged default behavior. Done: shared mechanisms exist with unchanged default behavior.
Completion evidence: `checkVersion` remains the existing diagnostic-only entry
point and now delegates to private scalar interpretation and callback-reporting
helpers through a result that keeps the interpreted `Version` separate from
diagnostics. Checker invocation and returned-error fallback use diagnostic
severity, preserving warnings without treating them as failures. Added a
regression for a warning followed by a returned checker error. These checks
passed with Go `go1.26.8 linux/amd64`:
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'Version|Validation'`
- `GOTOOLCHAIN=local GOPROXY=off go test . -run 'Version'`
- `GOTOOLCHAIN=local ./scripts/check.sh`
No environmental limitation affected validation; checks ran on Linux/amd64.
## Stage 3 — Implement source-local extraction and exact consumption ## Stage 3 — Implement source-local extraction and exact consumption
1. Add a private processor receiving original document, local rule, source role, 1. Add a private processor receiving original document, local rule, source role,

View File

@@ -463,6 +463,41 @@ func TestLoadVersionReportSuppressesReturnedErrorTextAndDefaults(t *testing.T) {
} }
} }
func TestVersionWarningDoesNotSuppressReturnedError(t *testing.T) {
t.Parallel()
type config struct{}
versionPath := Keys("schema_version")
result, err := testLoad(Bytes("version.yml", []byte("{}\n")), Options[config]{
Definition: Definition[config]{Version: &VersionRule{
Path: versionPath,
Check: func(_ Version, validation *Validation) error {
validation.Warning(versionPath, "review schema compatibility")
return errors.New("CANARY_VERSION_ERROR")
},
}},
})
if result != nil || err == nil {
t.Fatalf("result = %#v, err = %v", result, err)
}
var loadError *LoadError
if !errors.As(err, &loadError) {
t.Fatalf("error type = %T", err)
}
diagnostics := loadError.Diagnostics()
var warning, hookFailure bool
for _, diagnostic := range diagnostics {
if strings.Contains(diagnostic.Message, "CANARY") {
t.Fatalf("returned checker error text was exposed: %#v", diagnostic)
}
warning = warning || diagnostic.Code == CodeVersionWarning && diagnostic.Severity == SeverityWarning
hookFailure = hookFailure || diagnostic.Code == CodeVersionHookFailed && diagnostic.Severity == SeverityError
}
if !warning || !hookFailure {
t.Fatalf("diagnostics = %#v", diagnostics)
}
}
func TestLoadMetadataReflectsDefaultsInputAndDetachedRecords(t *testing.T) { func TestLoadMetadataReflectsDefaultsInputAndDetachedRecords(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -187,34 +187,59 @@ func validationValueMutations(before, after *document.Node) []Diagnostic {
return diagnostics return diagnostics
} }
type versionCheckResult struct {
version Version
diagnostics []Diagnostic
}
func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic { func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic {
return checkVersionResult(rule, root).diagnostics
}
func checkVersionResult(rule *VersionRule, root *document.Node) versionCheckResult {
if rule == nil { if rule == nil {
return nil return versionCheckResult{}
} }
version := Version{} result := interpretVersion(rule, root)
if hasErrors(result.diagnostics) {
result.diagnostics = canonicalDiagnostics(result.diagnostics)
return result
}
result.diagnostics = append(result.diagnostics, reportVersion(rule, result.version, root)...)
result.diagnostics = canonicalDiagnostics(result.diagnostics)
return result
}
func interpretVersion(rule *VersionRule, root *document.Node) versionCheckResult {
result := versionCheckResult{}
node, present := documentAtPath(root, rule.Path) node, present := documentAtPath(root, rule.Path)
if present { if present {
version.Present = true result.version.Present = true
switch node.Kind { switch node.Kind {
case document.Null: case document.Null:
version.Null = true result.version.Null = true
case document.String: case document.String:
version.Text = node.Text result.version.Text = node.Text
case document.Number: case document.Number:
if isIntegerVersion(node) { if isIntegerVersion(node) {
version.Text = node.Text result.version.Text = node.Text
break return result
} }
fallthrough fallthrough
default: default:
location, _ := nearestDocumentLocation(root, rule.Path) location, _ := nearestDocumentLocation(root, rule.Path)
primary := documentLocationPointer(location) primary := documentLocationPointer(location)
return []Diagnostic{{ result.diagnostics = []Diagnostic{{
Code: CodeVersionType, Severity: SeverityError, Path: rule.Path.clone(), Code: CodeVersionType, Severity: SeverityError, Path: rule.Path.clone(),
Message: "schema version must be a string, integer, or null", Primary: primary, Message: "schema version must be a string, integer, or null", Primary: primary,
}} }}
return result
} }
} }
return result
}
func reportVersion(rule *VersionRule, version Version, root *document.Node) []Diagnostic {
state := &validationState{ state := &validationState{
active: true, errorCode: CodeVersionError, warningCode: CodeVersionWarning, active: true, errorCode: CodeVersionError, warningCode: CodeVersionWarning,
invalidPath: CodeVersionError, document: root, invalidPath: CodeVersionError, document: root,
@@ -229,7 +254,7 @@ func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic {
Message: "schema-version check failed", Primary: documentLocationPointer(location), Message: "schema-version check failed", Primary: documentLocationPointer(location),
}) })
} }
return canonicalDiagnostics(state.diagnostics) return state.diagnostics
} }
func isIntegerVersion(node *document.Node) bool { func isIntegerVersion(node *document.Node) bool {