Build validation filter sets once per invocation

This commit is contained in:
2026-11-07 12:07:50 +00:00
parent 3223b5cb82
commit d72c9a788f
3 changed files with 59 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
# Shared reporting and PromptKit adoption remediation
Status: three open findings; R1–R4 complete. R1–R3 are from the architectural review of
Status: two open findings; R1–R5 complete. R1–R3 are from the architectural review of
`f83866e44a8a9066db52cbd2aa5cccde0d69a839`. R4–R7 are from the algorithmic
audit of `f5b2d1f80c4720b762f3301c29d5050710c210d0`; R4 is a correctness
issue and R5–R7 are efficiency opportunities for consideration.
@@ -211,6 +211,13 @@ on quoting style.
## R5 — P3: Validation filter membership rescans operator lists
**Complete.** Severity and code membership sets are built once per invocation
and shared by all reached scopes. Membership work is now expected O(C+V+D),
with O(C+V) storage, excluding string and existing projection costs. Expanded
public command checks preserve match-any/match-both semantics, duplicates, unknown
codes, ordering, evidence states, source references, and failure outcomes. The
independent ckconfig suite passes with Go 1.27.0.
**Location:** [validate.go](../../integrations/ckconfig/validate.go),
projectValidationDetails lines 128–133.

View File

@@ -60,6 +60,44 @@ func TestDetailedValidationFiltersAndRetainsFailureEvidence(t *testing.T) {
if strings.Contains(diag.String(), "native warnings") || !strings.Contains(diag.String(), "application warning") || !strings.Contains(diag.String(), "terminal failure") {
t.Fatalf("diagnostics=%q", diag.String())
}
for _, tc := range []struct {
name string
flags []string
first, second []string
}{
{"any within filters", []string{"--severity=warning", "--severity=error", "--code=chosen", "--code=other", "--code=failed"}, []string{"selected warning", "other warning"}, []string{"failed finding"}},
{"duplicates and unknown", []string{"--severity=warning", "--severity=warning", "--code=chosen", "--code=chosen", "--code=future-code"}, []string{"selected warning"}, nil},
{"both filters", []string{"--severity=error", "--code=chosen", "--code=failed"}, nil, []string{"failed finding"}},
{"no matching code", []string{"--code=future-code"}, nil, nil},
} {
t.Run(tc.name, func(t *testing.T) {
var out bytes.Buffer
args := append([]string{"config", "validate", "--scope=all", "--details", "--format=json"}, tc.flags...)
got := app.Execute(context.Background(), args, commandkit.Streams{Out: &out, Err: io.Discard})
var report validationReport
if err := json.Unmarshal(out.Bytes(), &report); err != nil {
t.Fatal(err)
}
if !errors.Is(got.Cause, cause) || len(report.Scopes) != 2 {
t.Fatalf("outcome=%#v report=%#v", got, report)
}
for index, want := range [][]string{tc.first, tc.second} {
details := report.Scopes[index].Details
if len(details.Findings) != len(want) {
t.Fatalf("findings=%#v want=%v", details.Findings, want)
}
for i, message := range want {
if details.Findings[i].Message != message {
t.Fatalf("finding order=%#v want=%v", details.Findings, want)
}
}
if len(want) == 0 && details.State != "filtered" {
t.Fatalf("state=%q", details.State)
}
}
})
}
before := len(calls)
for _, args := range [][]string{{"--severity=warning"}, {"--code=x"}, {"--details", "--severity=unknown"}} {
result = app.Execute(context.Background(), append([]string{"config", "validate"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})

View File

@@ -3,7 +3,6 @@ package ckconfig
import (
"errors"
"fmt"
"slices"
"text/template"
commandkit "go.maximumdirect.net/commandkit"
@@ -62,6 +61,14 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
reporter := invocation.Reporter()
report := validationReport{Scopes: make([]validationScope, 0, len(plan.scopes))}
outcome := commandkit.Success()
severities := make(map[configkit.Severity]bool, len(plan.severities))
for _, severity := range plan.severities {
severities[severity] = true
}
codes := make(map[string]bool, len(plan.codes))
for _, code := range plan.codes {
codes[code] = true
}
for _, selected := range plan.scopes {
if err := invocation.Context.Err(); err != nil {
outcome = configFailure("Configuration validation was canceled.", commandkit.Explanation{}, err)
@@ -71,7 +78,7 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
if selected.scope.scope.Acquire == nil {
entry := validationScope{Scope: name, Outcome: "failed"}
if plan.details {
entry.Details = projectValidationDetails(View{}, plan.severities, plan.codes)
entry.Details = projectValidationDetails(View{}, severities, codes)
}
report.Scopes = append(report.Scopes, entry)
outcome = commandkit.Failure(fmt.Sprintf("Configuration scope %q has no acquisition callback.", name))
@@ -84,7 +91,7 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
}
entry := validationScope{Scope: name, Outcome: "valid"}
if plan.details {
entry.Details = projectValidationDetails(acquired.Value, plan.severities, plan.codes)
entry.Details = projectValidationDetails(acquired.Value, severities, codes)
}
if canceled := invocation.Context.Err(); canceled != nil {
entry.Outcome = "canceled"
@@ -114,7 +121,7 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
return outcome
}
func projectValidationDetails(view View, severities []configkit.Severity, codes []string) *validationDetails {
func projectValidationDetails(view View, severities map[configkit.Severity]bool, codes map[string]bool) *validationDetails {
details := &validationDetails{State: "unavailable", Findings: []validationFinding{}, Sources: []validationSource{}}
if view.Findings == nil {
return details
@@ -126,10 +133,10 @@ func projectValidationDetails(view View, severities []configkit.Severity, codes
details.State = "filtered"
references := make(map[string]bool)
for _, record := range view.Findings.Records {
if len(severities) > 0 && !slices.Contains(severities, record.Severity) {
if len(severities) > 0 && !severities[record.Severity] {
continue
}
if len(codes) > 0 && !slices.Contains(codes, string(record.Code)) {
if len(codes) > 0 && !codes[string(record.Code)] {
continue
}
finding := validationFinding{Code: string(record.Code), Severity: string(record.Severity), Path: record.Path.String(), Message: record.Message, Related: locationReports(record.Related)}