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:
Benjamin Admin
2026-06-25 09:50:37 +02:00
parent 2f3c98fbe0
commit 53ea388ea0
3 changed files with 85 additions and 64 deletions
@@ -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)
}
}