FLEX-WP-0031-T03: durable decision outbox and the FLEX-DEC-2026-018 release rule
internal/emission commits one audit-core-shaped event per decision with fsync before release (one sync per batch), random event ids with the decision id as correlation_id, torn-tail and failed-commit truncation, and per-class committed/released_uncommitted counts at GET /v1/emission. The engine releases restrictions whose record failed to commit and withholds allow/audit_only (503). api.DecisionEffects() is pinned by a source-parsing test and cadence.yaml must classify exactly it. T03 split under the task budget: heartbeat+drain is T05, reconciliation, profile check and PVC are T06. Nothing deployed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 307130@bnt-lap001 Assistant-Session: 270c79f7-0823-4b0d-990d-aad5af9935ce
This commit is contained in:
parent
7f0e2e37f9
commit
cd14a34e33
9 changed files with 842 additions and 23 deletions
|
|
@ -209,6 +209,30 @@ const (
|
|||
DecisionEffectNotApplicable DecisionEffect = "not_applicable"
|
||||
)
|
||||
|
||||
// DecisionEffects returns the complete effect vocabulary. Every effect is an
|
||||
// emission class in cadence.yaml; a test fails if a constant above is missing
|
||||
// here, so a new effect cannot ship unclassified (FLEX-WP-0031-T03).
|
||||
func DecisionEffects() []DecisionEffect {
|
||||
return []DecisionEffect{
|
||||
DecisionEffectAllow,
|
||||
DecisionEffectDeny,
|
||||
DecisionEffectRedact,
|
||||
DecisionEffectAuditOnly,
|
||||
DecisionEffectNotApplicable,
|
||||
}
|
||||
}
|
||||
|
||||
// Restricts reports whether an effect withholds authority. A restriction is
|
||||
// released even when its record fails to commit, because withholding it turns
|
||||
// it into an error an open-stance consumer reads as proceed (FLEX-DEC-2026-018).
|
||||
func (e DecisionEffect) Restricts() bool {
|
||||
switch e {
|
||||
case DecisionEffectDeny, DecisionEffectRedact, DecisionEffectNotApplicable:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DecisionRecordContractV1 is the published decision-record contract identifier.
|
||||
const DecisionRecordContractV1 = "flex-auth.decision-record.v1"
|
||||
|
||||
|
|
|
|||
67
pkg/api/effects_test.go
Normal file
67
pkg/api/effects_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDecisionEffectsListsEveryConstant parses canonical.go so that adding a
|
||||
// DecisionEffect constant without listing it in DecisionEffects fails here.
|
||||
func TestDecisionEffectsListsEveryConstant(t *testing.T) {
|
||||
file, err := parser.ParseFile(token.NewFileSet(), "canonical.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declared := map[string]bool{}
|
||||
ast.Inspect(file, func(node ast.Node) bool {
|
||||
spec, ok := node.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if ident, ok := spec.Type.(*ast.Ident); !ok || ident.Name != "DecisionEffect" {
|
||||
return true
|
||||
}
|
||||
for _, value := range spec.Values {
|
||||
if lit, ok := value.(*ast.BasicLit); ok {
|
||||
declared[lit.Value[1:len(lit.Value)-1]] = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
listed := map[string]bool{}
|
||||
for _, effect := range DecisionEffects() {
|
||||
listed[string(effect)] = true
|
||||
}
|
||||
if len(declared) == 0 {
|
||||
t.Fatal("found no DecisionEffect constants in canonical.go")
|
||||
}
|
||||
for effect := range declared {
|
||||
if !listed[effect] {
|
||||
t.Errorf("DecisionEffect %q is declared but missing from DecisionEffects()", effect)
|
||||
}
|
||||
}
|
||||
if len(listed) != len(declared) {
|
||||
t.Errorf("DecisionEffects() lists %d effects, canonical.go declares %d", len(listed), len(declared))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestrictsSplitsByDirection(t *testing.T) {
|
||||
want := map[DecisionEffect]bool{
|
||||
DecisionEffectAllow: false,
|
||||
DecisionEffectAuditOnly: false,
|
||||
DecisionEffectDeny: true,
|
||||
DecisionEffectRedact: true,
|
||||
DecisionEffectNotApplicable: true,
|
||||
}
|
||||
for _, effect := range DecisionEffects() {
|
||||
expected, ok := want[effect]
|
||||
if !ok {
|
||||
t.Fatalf("effect %q has no stated direction; decide it under FLEX-DEC-2026-018", effect)
|
||||
}
|
||||
if effect.Restricts() != expected {
|
||||
t.Errorf("%q.Restricts() = %v, want %v", effect, effect.Restricts(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue