Build validation filter sets once per invocation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit adoption remediation
|
# 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
|
`f83866e44a8a9066db52cbd2aa5cccde0d69a839`. R4–R7 are from the algorithmic
|
||||||
audit of `f5b2d1f80c4720b762f3301c29d5050710c210d0`; R4 is a correctness
|
audit of `f5b2d1f80c4720b762f3301c29d5050710c210d0`; R4 is a correctness
|
||||||
issue and R5–R7 are efficiency opportunities for consideration.
|
issue and R5–R7 are efficiency opportunities for consideration.
|
||||||
@@ -211,6 +211,13 @@ on quoting style.
|
|||||||
|
|
||||||
## R5 — P3: Validation filter membership rescans operator lists
|
## 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),
|
**Location:** [validate.go](../../integrations/ckconfig/validate.go),
|
||||||
projectValidationDetails lines 128–133.
|
projectValidationDetails lines 128–133.
|
||||||
|
|
||||||
|
|||||||
@@ -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") {
|
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())
|
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)
|
before := len(calls)
|
||||||
for _, args := range [][]string{{"--severity=warning"}, {"--code=x"}, {"--details", "--severity=unknown"}} {
|
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})
|
result = app.Execute(context.Background(), append([]string{"config", "validate"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package ckconfig
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
commandkit "go.maximumdirect.net/commandkit"
|
commandkit "go.maximumdirect.net/commandkit"
|
||||||
@@ -62,6 +61,14 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
|
|||||||
reporter := invocation.Reporter()
|
reporter := invocation.Reporter()
|
||||||
report := validationReport{Scopes: make([]validationScope, 0, len(plan.scopes))}
|
report := validationReport{Scopes: make([]validationScope, 0, len(plan.scopes))}
|
||||||
outcome := commandkit.Success()
|
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 {
|
for _, selected := range plan.scopes {
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
outcome = configFailure("Configuration validation was canceled.", commandkit.Explanation{}, err)
|
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 {
|
if selected.scope.scope.Acquire == nil {
|
||||||
entry := validationScope{Scope: name, Outcome: "failed"}
|
entry := validationScope{Scope: name, Outcome: "failed"}
|
||||||
if plan.details {
|
if plan.details {
|
||||||
entry.Details = projectValidationDetails(View{}, plan.severities, plan.codes)
|
entry.Details = projectValidationDetails(View{}, severities, codes)
|
||||||
}
|
}
|
||||||
report.Scopes = append(report.Scopes, entry)
|
report.Scopes = append(report.Scopes, entry)
|
||||||
outcome = commandkit.Failure(fmt.Sprintf("Configuration scope %q has no acquisition callback.", name))
|
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"}
|
entry := validationScope{Scope: name, Outcome: "valid"}
|
||||||
if plan.details {
|
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 {
|
if canceled := invocation.Context.Err(); canceled != nil {
|
||||||
entry.Outcome = "canceled"
|
entry.Outcome = "canceled"
|
||||||
@@ -114,7 +121,7 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
|
|||||||
return outcome
|
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{}}
|
details := &validationDetails{State: "unavailable", Findings: []validationFinding{}, Sources: []validationSource{}}
|
||||||
if view.Findings == nil {
|
if view.Findings == nil {
|
||||||
return details
|
return details
|
||||||
@@ -126,10 +133,10 @@ func projectValidationDetails(view View, severities []configkit.Severity, codes
|
|||||||
details.State = "filtered"
|
details.State = "filtered"
|
||||||
references := make(map[string]bool)
|
references := make(map[string]bool)
|
||||||
for _, record := range view.Findings.Records {
|
for _, record := range view.Findings.Records {
|
||||||
if len(severities) > 0 && !slices.Contains(severities, record.Severity) {
|
if len(severities) > 0 && !severities[record.Severity] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(codes) > 0 && !slices.Contains(codes, string(record.Code)) {
|
if len(codes) > 0 && !codes[string(record.Code)] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
finding := validationFinding{Code: string(record.Code), Severity: string(record.Severity), Path: record.Path.String(), Message: record.Message, Related: locationReports(record.Related)}
|
finding := validationFinding{Code: string(record.Code), Severity: string(record.Severity), Path: record.Path.String(), Message: record.Message, Related: locationReports(record.Related)}
|
||||||
|
|||||||
Reference in New Issue
Block a user