refactor(ucca): control-mapping model per review feedback
- DROP confidence from the persisted mapping: a curated mapping is a professional statement, not an AI guess (retriever score -> rationale only). - ADD mapping_status (candidate|accepted|rejected|superseded) — the review state. - ADD audit trail (reviewed_by/review_date/review_reason); accepted/rejected fail-closed without it. - EXTEND mapping_type: + implements, + contradicts. - Advisor truth = mapping_status=accepted (acceptedOnly filter). - migrate the 18 CRA->OWASP rows to mapping_status=candidate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,31 +9,39 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ControlMapping is one persisted, versioned link from a legal obligation/requirement
|
||||
// to a concrete framework control. The retriever only PROPOSES candidates
|
||||
// (provenance=retriever_candidate); the curated mapping (human_curated/rule_based) is the
|
||||
// audited truth the Advisor uses at runtime — never re-invented per query.
|
||||
// ControlMapping is one persisted, versioned, REVIEWABLE link from a legal
|
||||
// obligation/requirement to a concrete framework control — a node in the curated
|
||||
// compliance graph (Regulation -> Obligation -> Control -> Evidence). The retriever only
|
||||
// PROPOSES candidates (mapping_status=candidate); a human/rule decision turns the good ones
|
||||
// into mapping_status=accepted, which is the audited truth the Advisor uses at runtime.
|
||||
//
|
||||
// There is intentionally NO probabilistic "confidence" field: once curated, a mapping is a
|
||||
// professional statement, not an AI guess. The retriever's score lives only in the rationale
|
||||
// of a candidate, never as structured truth.
|
||||
type ControlMapping struct {
|
||||
SourceNorm string `json:"source_norm"` // e.g. "CRA Annex I Part II"
|
||||
SourceNorm string `json:"source_norm"` // e.g. "CRA Annex I Part I (2)(c)"
|
||||
SourceRole string `json:"source_role"` // source_role of the norm (operational_requirement, ...)
|
||||
TargetFramework string `json:"target_framework"` // e.g. "OWASP ASVS"
|
||||
TargetControl string `json:"target_control"` // e.g. "V6.2.4"
|
||||
MappingType string `json:"mapping_type"` // supports | partially_supports | evidence_for | related
|
||||
Confidence string `json:"confidence"` // high | medium | low
|
||||
TargetControl string `json:"target_control"` // e.g. "V6.3.1"
|
||||
MappingType string `json:"mapping_type"` // supports | partially_supports | implements | related | contradicts
|
||||
MappingStatus string `json:"mapping_status"` // candidate | accepted | rejected | superseded
|
||||
Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based
|
||||
Rationale string `json:"rationale"`
|
||||
Version string `json:"version"` // YYYY-MM-DD
|
||||
ReviewedBy string `json:"reviewed_by,omitempty"` // who decided (human or rule id)
|
||||
ReviewDate string `json:"review_date,omitempty"` // YYYY-MM-DD
|
||||
ReviewReason string `json:"review_reason,omitempty"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Allowed enum values — the deterministic "rule" layer that keeps the curated store clean.
|
||||
var (
|
||||
mappingTypeValues = map[string]bool{"supports": true, "partially_supports": true, "evidence_for": true, "related": true}
|
||||
confidenceValues = map[string]bool{"high": true, "medium": true, "low": true}
|
||||
provenanceValues = map[string]bool{"retriever_candidate": true, "human_curated": true, "rule_based": true}
|
||||
mappingTypeValues = map[string]bool{"supports": true, "partially_supports": true, "implements": true, "related": true, "contradicts": true}
|
||||
mappingStatusValues = map[string]bool{"candidate": true, "accepted": true, "rejected": true, "superseded": true}
|
||||
provenanceValues = map[string]bool{"retriever_candidate": true, "human_curated": true, "rule_based": true}
|
||||
)
|
||||
|
||||
// Validate checks required fields + enum membership, so the persisted audit store never
|
||||
// holds garbage (fail-closed at load).
|
||||
// Validate checks required fields + enum membership, and enforces the audit trail: any
|
||||
// human/rule DECISION (accepted/rejected) must carry who/when/why. Fail-closed at load.
|
||||
func (m ControlMapping) Validate() error {
|
||||
switch {
|
||||
case m.SourceNorm == "":
|
||||
@@ -44,18 +52,22 @@ func (m ControlMapping) Validate() error {
|
||||
return fmt.Errorf("control mapping: target_control required")
|
||||
case !mappingTypeValues[m.MappingType]:
|
||||
return fmt.Errorf("control mapping: invalid mapping_type %q", m.MappingType)
|
||||
case !confidenceValues[m.Confidence]:
|
||||
return fmt.Errorf("control mapping: invalid confidence %q", m.Confidence)
|
||||
case !mappingStatusValues[m.MappingStatus]:
|
||||
return fmt.Errorf("control mapping: invalid mapping_status %q", m.MappingStatus)
|
||||
case !provenanceValues[m.Provenance]:
|
||||
return fmt.Errorf("control mapping: invalid provenance %q", m.Provenance)
|
||||
}
|
||||
if m.MappingStatus == "accepted" || m.MappingStatus == "rejected" {
|
||||
if m.ReviewedBy == "" || m.ReviewDate == "" || m.ReviewReason == "" {
|
||||
return fmt.Errorf("control mapping %s->%s: status %q requires reviewed_by + review_date + review_reason (audit trail)",
|
||||
m.SourceNorm, m.TargetControl, m.MappingStatus)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsCurated reports whether this mapping is part of the audited truth (not a raw candidate).
|
||||
func (m ControlMapping) IsCurated() bool {
|
||||
return m.Provenance == "human_curated" || m.Provenance == "rule_based"
|
||||
}
|
||||
// IsAccepted reports whether this mapping is the active audited truth.
|
||||
func (m ControlMapping) IsAccepted() bool { return m.MappingStatus == "accepted" }
|
||||
|
||||
// ControlMappingSet is the loaded, indexed mapping store (forward + reverse lookup).
|
||||
type ControlMappingSet struct {
|
||||
@@ -66,24 +78,24 @@ type ControlMappingSet struct {
|
||||
|
||||
func controlKey(framework, control string) string { return framework + ":" + control }
|
||||
|
||||
// ControlsFor returns the controls mapped to a source norm. curatedOnly restricts to the
|
||||
// ControlsFor returns the controls mapped to a source norm. acceptedOnly restricts to the
|
||||
// audited truth (what the Advisor may treat as fact).
|
||||
func (s *ControlMappingSet) ControlsFor(sourceNorm string, curatedOnly bool) []ControlMapping {
|
||||
return filterProvenance(s.bySourceNorm[sourceNorm], curatedOnly)
|
||||
func (s *ControlMappingSet) ControlsFor(sourceNorm string, acceptedOnly bool) []ControlMapping {
|
||||
return filterAccepted(s.bySourceNorm[sourceNorm], acceptedOnly)
|
||||
}
|
||||
|
||||
// ObligationsFor returns the norms mapped to a framework control (reverse lookup).
|
||||
func (s *ControlMappingSet) ObligationsFor(framework, control string, curatedOnly bool) []ControlMapping {
|
||||
return filterProvenance(s.byControl[controlKey(framework, control)], curatedOnly)
|
||||
func (s *ControlMappingSet) ObligationsFor(framework, control string, acceptedOnly bool) []ControlMapping {
|
||||
return filterAccepted(s.byControl[controlKey(framework, control)], acceptedOnly)
|
||||
}
|
||||
|
||||
func filterProvenance(in []ControlMapping, curatedOnly bool) []ControlMapping {
|
||||
if !curatedOnly {
|
||||
func filterAccepted(in []ControlMapping, acceptedOnly bool) []ControlMapping {
|
||||
if !acceptedOnly {
|
||||
return in
|
||||
}
|
||||
out := make([]ControlMapping, 0, len(in))
|
||||
for _, m := range in {
|
||||
if m.IsCurated() {
|
||||
if m.IsAccepted() {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,25 @@ import (
|
||||
)
|
||||
|
||||
func TestControlMapping_Validate(t *testing.T) {
|
||||
valid := ControlMapping{SourceNorm: "CRA Annex I", TargetFramework: "OWASP ASVS", TargetControl: "V6.2.4", MappingType: "supports", Confidence: "high", Provenance: "human_curated"}
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("valid mapping rejected: %v", err)
|
||||
candidate := ControlMapping{SourceNorm: "CRA Annex I", TargetFramework: "OWASP ASVS", TargetControl: "V6.3.1", MappingType: "supports", MappingStatus: "candidate", Provenance: "retriever_candidate"}
|
||||
if err := candidate.Validate(); err != nil {
|
||||
t.Fatalf("valid candidate rejected: %v", err)
|
||||
}
|
||||
accepted := ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "implements", MappingStatus: "accepted", Provenance: "human_curated", ReviewedBy: "benjamin", ReviewDate: "2026-06-25", ReviewReason: "passt"}
|
||||
if err := accepted.Validate(); err != nil {
|
||||
t.Fatalf("valid accepted rejected: %v", err)
|
||||
}
|
||||
|
||||
bad := []struct {
|
||||
name string
|
||||
m ControlMapping
|
||||
}{
|
||||
{"no source_norm", ControlMapping{TargetFramework: "X", TargetControl: "Y", MappingType: "supports", Confidence: "high", Provenance: "human_curated"}},
|
||||
{"no target_control", ControlMapping{SourceNorm: "A", TargetFramework: "X", MappingType: "supports", Confidence: "high", Provenance: "human_curated"}},
|
||||
{"bad mapping_type", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "nope", Confidence: "high", Provenance: "human_curated"}},
|
||||
{"bad confidence", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", Confidence: "huge", Provenance: "human_curated"}},
|
||||
{"bad provenance", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", Confidence: "high", Provenance: "guessed"}},
|
||||
{"no source_norm", ControlMapping{TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "candidate", Provenance: "retriever_candidate"}},
|
||||
{"bad mapping_type", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "nope", MappingStatus: "candidate", Provenance: "retriever_candidate"}},
|
||||
{"bad mapping_status", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "maybe", Provenance: "retriever_candidate"}},
|
||||
{"bad provenance", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "candidate", Provenance: "guessed"}},
|
||||
{"accepted without audit trail", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "accepted", Provenance: "human_curated"}},
|
||||
{"rejected without reason", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "rejected", Provenance: "human_curated", ReviewedBy: "b", ReviewDate: "2026-06-25"}},
|
||||
}
|
||||
for _, tt := range bad {
|
||||
if err := tt.m.Validate(); err == nil {
|
||||
@@ -31,8 +37,8 @@ func TestControlMapping_Validate(t *testing.T) {
|
||||
func TestLoadControlMappings(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := `// header comment, ignored
|
||||
{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V11.1.1","mapping_type":"supports","confidence":"high","provenance":"human_curated","rationale":"r","version":"2026-06-25"}
|
||||
{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V6.2.4","mapping_type":"related","confidence":"low","provenance":"retriever_candidate","rationale":"r","version":"2026-06-25"}
|
||||
{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V6.3.1","mapping_type":"supports","mapping_status":"accepted","provenance":"human_curated","reviewed_by":"benjamin","review_date":"2026-06-25","review_reason":"V6=Auth passt","rationale":"r","version":"2026-06-25"}
|
||||
{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V14.2.4","mapping_type":"related","mapping_status":"candidate","provenance":"retriever_candidate","rationale":"r","version":"2026-06-25"}
|
||||
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "m.jsonl"), []byte(content), 0o644); err != nil {
|
||||
@@ -49,20 +55,21 @@ func TestLoadControlMappings(t *testing.T) {
|
||||
t.Errorf("ControlsFor(all): want 2, got %d", len(got))
|
||||
}
|
||||
if got := set.ControlsFor("CRA Annex I", true); len(got) != 1 {
|
||||
t.Errorf("ControlsFor(curatedOnly): want 1 (only human_curated), got %d", len(got))
|
||||
t.Errorf("ControlsFor(acceptedOnly): want 1 (only accepted), got %d", len(got))
|
||||
}
|
||||
if got := set.ObligationsFor("OWASP ASVS", "V11.1.1", false); len(got) != 1 {
|
||||
t.Errorf("ObligationsFor reverse lookup: want 1, got %d", len(got))
|
||||
if got := set.ObligationsFor("OWASP ASVS", "V6.3.1", true); len(got) != 1 {
|
||||
t.Errorf("ObligationsFor accepted reverse lookup: want 1, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadControlMappings_RejectsInvalid(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "bad.jsonl"), []byte(`{"source_norm":"A","target_framework":"X","target_control":"Y","mapping_type":"BOGUS","confidence":"high","provenance":"human_curated"}`), 0o644); err != nil {
|
||||
// accepted without the who/when/why audit trail must fail-closed.
|
||||
if err := os.WriteFile(filepath.Join(dir, "bad.jsonl"), []byte(`{"source_norm":"A","target_framework":"X","target_control":"Y","mapping_type":"supports","mapping_status":"accepted","provenance":"human_curated","rationale":"r","version":"v"}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadControlMappings(dir); err == nil {
|
||||
t.Error("invalid mapping_type must fail the load (fail-closed audit store)")
|
||||
t.Error("accepted mapping without audit trail must fail the load (fail-closed)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user