feat: migrate refimpls from draft-00 to draft-01 claim names

- Rename `par` to `pred` (predecessor) in types, serialization, tests
- Remove `pol`, `pol_decision` from core payload; move to `ect_ext`
- Remove `sub` from payload (not part of ECT spec)
- Update `typ` from `wimse-exec+jwt` to `exec+jwt` (accept both)
- Rename MaxParLength to MaxPredLength everywhere
- Update testdata, demos, READMEs with migration table
- All Go tests pass, all 56 Python tests pass (90% coverage)
This commit is contained in:
2026-04-03 10:55:58 +02:00
parent ba044f6626
commit 884d2dc836
33 changed files with 416 additions and 481 deletions

View File

@@ -24,7 +24,7 @@ type ECTStore interface {
type DAGConfig struct {
ClockSkewTolerance int // seconds; recommended 30
MaxAncestorLimit int // recommended 10000
MaxParLength int // max par length (0 = no limit; recommended 100)
MaxPredLength int // max pred length (0 = no limit; recommended 100)
}
// DefaultDAGConfig returns recommended defaults.
@@ -32,7 +32,7 @@ func DefaultDAGConfig() DAGConfig {
return DAGConfig{
ClockSkewTolerance: DefaultClockSkewTolerance,
MaxAncestorLimit: DefaultMaxAncestorLimit,
MaxParLength: DefaultMaxParLength,
MaxPredLength: DefaultMaxPredLength,
}
}
@@ -48,8 +48,8 @@ func ValidateDAG(ect *Payload, store ECTStore, cfg DAGConfig) error {
if cfg.MaxAncestorLimit <= 0 {
cfg.MaxAncestorLimit = DefaultMaxAncestorLimit
}
if cfg.MaxParLength > 0 && len(ect.Par) > cfg.MaxParLength {
return ErrParLength
if cfg.MaxPredLength > 0 && len(ect.Pred) > cfg.MaxPredLength {
return ErrPredLength
}
// 1. Task ID Uniqueness (task id = jti per spec)
@@ -57,31 +57,33 @@ func ValidateDAG(ect *Payload, store ECTStore, cfg DAGConfig) error {
return fmt.Errorf("ect: task ID (jti) already exists: %s", ect.Jti)
}
// 2. Parent Existence and 3. Temporal Ordering
for _, parentID := range ect.Par {
parent := store.GetByTid(parentID)
if parent == nil {
return fmt.Errorf("ect: parent task not found: %s", parentID)
// 2. Predecessor Existence and 3. Temporal Ordering
for _, predID := range ect.Pred {
pred := store.GetByTid(predID)
if pred == nil {
return fmt.Errorf("ect: predecessor task not found: %s", predID)
}
// parent.iat < child.iat + clock_skew_tolerance => parent.iat - ect.iat <= clock_skew_tolerance
if parent.Iat >= ect.Iat+int64(cfg.ClockSkewTolerance) {
return fmt.Errorf("ect: parent task not earlier than current: %s", parentID)
// pred.iat < child.iat + clock_skew_tolerance
if pred.Iat >= ect.Iat+int64(cfg.ClockSkewTolerance) {
return fmt.Errorf("ect: predecessor task not earlier than current: %s", predID)
}
}
// 4. Acyclicity (and depth limit)
visited := make(map[string]struct{})
if hasCycle(ect.Jti, ect.Par, store, visited, cfg.MaxAncestorLimit) {
if hasCycle(ect.Jti, ect.Pred, store, visited, cfg.MaxAncestorLimit) {
return errors.New("ect: circular dependency or depth limit exceeded")
}
// 5. Parent Policy Decision (only when parent has policy claims per spec)
for _, parentID := range ect.Par {
parent := store.GetByTid(parentID)
if parent != nil && parent.HasPolicyClaims() &&
(parent.PolDecision == PolDecisionRejected || parent.PolDecision == PolDecisionPendingHumanReview) {
if !ect.CompensationRequired() {
return errors.New("ect: parent has non-approved pol_decision; current ECT must be compensation/remediation or have ext.compensation_required true")
// 5. Predecessor Policy Decision (only when predecessor has policy claims in ext per -01)
for _, predID := range ect.Pred {
pred := store.GetByTid(predID)
if pred != nil && pred.HasPolicyClaims() {
polDec := pred.PolDecision()
if polDec == "rejected" || polDec == "pending_human_review" {
if !ect.CompensationRequired() {
return errors.New("ect: predecessor has non-approved pol_decision; current ECT must be compensation/remediation or have ext.compensation_required true")
}
}
}
}
@@ -89,23 +91,23 @@ func ValidateDAG(ect *Payload, store ECTStore, cfg DAGConfig) error {
return nil
}
// hasCycle returns true if following par from the given parent IDs leads back to targetTid
// hasCycle returns true if following pred from the given predecessor IDs leads back to targetTid
// or if traversal exceeds maxDepth. visited is mutated.
func hasCycle(targetTid string, parentIDs []string, store ECTStore, visited map[string]struct{}, maxDepth int) bool {
func hasCycle(targetTid string, predIDs []string, store ECTStore, visited map[string]struct{}, maxDepth int) bool {
if len(visited) >= maxDepth {
return true
}
for _, parentID := range parentIDs {
if parentID == targetTid {
for _, predID := range predIDs {
if predID == targetTid {
return true
}
if _, ok := visited[parentID]; ok {
if _, ok := visited[predID]; ok {
continue
}
visited[parentID] = struct{}{}
parent := store.GetByTid(parentID)
if parent != nil {
if hasCycle(targetTid, parent.Par, store, visited, maxDepth) {
visited[predID] = struct{}{}
pred := store.GetByTid(predID)
if pred != nil {
if hasCycle(targetTid, pred.Pred, store, visited, maxDepth) {
return true
}
}