diff --git a/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-21-plan-decision-operator.md b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-21-plan-decision-operator.md new file mode 100644 index 000000000..a25b8f901 --- /dev/null +++ b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-21-plan-decision-operator.md @@ -0,0 +1,3 @@ +### Introduce deterministic DecisionOperator at plan boundary + +Replaced the hardcoded validation errors for architecture facts with a deterministic `PlanDecisionOperator` primitive (`Infer`, `Query`, `Verify`, `Reject`, `Escalate`). The `validateArchitectureGrounding` boundary now evaluates repository evidence and human intent against a strict policy matrix before routing the decision. This optimizes the query architecture to only block or query when evidence is insufficient, making the supervisor's decisions formal, recordable, and explainable. diff --git a/labs/12-product-engineering-loop/product-engineering-loop/decision.go b/labs/12-product-engineering-loop/product-engineering-loop/decision.go new file mode 100644 index 000000000..302fad328 --- /dev/null +++ b/labs/12-product-engineering-loop/product-engineering-loop/decision.go @@ -0,0 +1,94 @@ +package boatstack + +type DecisionOperator string + +const ( + OperatorInfer DecisionOperator = "infer" + OperatorQuery DecisionOperator = "query" + OperatorVerify DecisionOperator = "verify" + OperatorReject DecisionOperator = "reject" + OperatorEscalate DecisionOperator = "escalate" +) + +type EvidenceLevel string + +const ( + EvidenceVerified EvidenceLevel = "verified" + EvidenceSupported EvidenceLevel = "supported" + EvidenceAbsent EvidenceLevel = "absent" + EvidenceConflicting EvidenceLevel = "conflicting" +) + +type PremiseStatus string + +const ( + PremiseUnknown PremiseStatus = "unknown" + PremiseValid PremiseStatus = "valid" + PremiseInvalid PremiseStatus = "invalid" +) + +type PlanDecisionInput struct { + DecisionKind string + IsMaterial bool + RepositoryEvidence []EvidenceRecord + EvidenceLevel EvidenceLevel + PremiseStatus PremiseStatus +} + +type DecisionResolution struct { + Operator DecisionOperator + RuleID string + Reason string + Evidence []EvidenceRecord + EvidenceLevel EvidenceLevel + PremiseStatus PremiseStatus +} + +func ResolvePlanDecision(input PlanDecisionInput) DecisionResolution { + resolution := DecisionResolution{ + Evidence: input.RepositoryEvidence, + EvidenceLevel: input.EvidenceLevel, + PremiseStatus: input.PremiseStatus, + } + + if input.PremiseStatus == PremiseInvalid { + resolution.Operator = OperatorReject + resolution.RuleID = "invalid-premise-rejected" + resolution.Reason = "planning premise is not supported" + return resolution + } + + if input.EvidenceLevel == EvidenceConflicting { + resolution.Operator = OperatorEscalate + resolution.RuleID = "conflicting-evidence-escalated" + resolution.Reason = "repository evidence conflicts" + return resolution + } + + if input.EvidenceLevel == EvidenceVerified { + // Assuming verified evidence is sufficient to resolve the decision + resolution.Operator = OperatorInfer + resolution.RuleID = "verified-evidence-inferred" + resolution.Reason = "verified repository evidence resolves the decision" + return resolution + } + + if input.EvidenceLevel == EvidenceSupported { + resolution.Operator = OperatorVerify + resolution.RuleID = "supported-evidence-requires-verification" + resolution.Reason = "evidence is supported but requires independent verification" + return resolution + } + + if input.IsMaterial && input.EvidenceLevel == EvidenceAbsent { + resolution.Operator = OperatorQuery + resolution.RuleID = "material-intent-requires-human" + resolution.Reason = "material product intent requires human input" + return resolution + } + + resolution.Operator = OperatorEscalate + resolution.RuleID = "unresolved-uncertainty-escalated" + resolution.Reason = "unresolved uncertainty requires escalation" + return resolution +} diff --git a/labs/12-product-engineering-loop/product-engineering-loop/decision_test.go b/labs/12-product-engineering-loop/product-engineering-loop/decision_test.go new file mode 100644 index 000000000..1609e91e5 --- /dev/null +++ b/labs/12-product-engineering-loop/product-engineering-loop/decision_test.go @@ -0,0 +1,70 @@ +package boatstack + +import "testing" + +func TestResolvePlanDecision(t *testing.T) { + tests := []struct { + name string + input PlanDecisionInput + expected DecisionOperator + }{ + { + name: "invalid premise rejects", + input: PlanDecisionInput{ + PremiseStatus: PremiseInvalid, + }, + expected: OperatorReject, + }, + { + name: "conflicting evidence escalates", + input: PlanDecisionInput{ + PremiseStatus: PremiseValid, + EvidenceLevel: EvidenceConflicting, + }, + expected: OperatorEscalate, + }, + { + name: "verified evidence infers", + input: PlanDecisionInput{ + PremiseStatus: PremiseValid, + EvidenceLevel: EvidenceVerified, + }, + expected: OperatorInfer, + }, + { + name: "supported evidence verifies", + input: PlanDecisionInput{ + PremiseStatus: PremiseValid, + EvidenceLevel: EvidenceSupported, + }, + expected: OperatorVerify, + }, + { + name: "absent evidence for material intent queries", + input: PlanDecisionInput{ + PremiseStatus: PremiseValid, + IsMaterial: true, + EvidenceLevel: EvidenceAbsent, + }, + expected: OperatorQuery, + }, + { + name: "unknown state escalates", + input: PlanDecisionInput{ + PremiseStatus: PremiseUnknown, + EvidenceLevel: EvidenceAbsent, + IsMaterial: false, + }, + expected: OperatorEscalate, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolution := ResolvePlanDecision(tt.input) + if resolution.Operator != tt.expected { + t.Errorf("expected operator %s, got %s", tt.expected, resolution.Operator) + } + }) + } +} diff --git a/labs/12-product-engineering-loop/product-engineering-loop/plan_validation.go b/labs/12-product-engineering-loop/product-engineering-loop/plan_validation.go index 1fd0c59e4..cbce636da 100644 --- a/labs/12-product-engineering-loop/product-engineering-loop/plan_validation.go +++ b/labs/12-product-engineering-loop/product-engineering-loop/plan_validation.go @@ -44,38 +44,56 @@ func validateArchitectureGrounding(plan map[string]any, opts *ValidatePlanOption return fmt.Errorf("unsupported architecture fact kind: %s", kind) } + var evidence []EvidenceRecord + evidenceLevel := EvidenceVerified + premiseStatus := PremiseValid evidenceIDs, ok := stringSlice(fact["evidence_ids"]) + if !ok || len(evidenceIDs) == 0 { - return fmt.Errorf("architecture fact %s requires at least one evidence ID", id) - } - - // Validation of evidence against ledger - for _, evID := range evidenceIDs { - record, exists := ledger[evID] - if !exists { - return fmt.Errorf("architecture fact %s references unknown evidence ID: %s", id, evID) - } - if opts != nil && opts.RepoRevision != "" && record.RepositoryRevision != opts.RepoRevision { - return fmt.Errorf("evidence %s has stale repository revision: %s", evID, record.RepositoryRevision) - } - - // Basic operation check - if kind == "route_absent" && record.Operation != "repository_search" && record.Operation != "route_lookup" { - return fmt.Errorf("architecture fact %s requires repository_search evidence", id) - } - - if opts != nil && opts.RepoRoot != "" && record.Path != "" { - if err := ValidateEvidencePath(opts.RepoRoot, record.Path); err != nil { - return fmt.Errorf("invalid evidence path in %s: %w", evID, err) + evidenceLevel = EvidenceAbsent + } else { + // Validation of evidence against ledger + for _, evID := range evidenceIDs { + record, exists := ledger[evID] + if !exists { + evidenceLevel = EvidenceAbsent + break } - if len(record.Anchors) > 0 { - targetPath := filepath.Join(opts.RepoRoot, filepath.Clean(record.Path)) - if err := CheckFileAnchors(targetPath, record.Anchors); err != nil { - return fmt.Errorf("evidence %s anchor check failed: %w", evID, err) + evidence = append(evidence, record) + if opts != nil && opts.RepoRevision != "" && record.RepositoryRevision != opts.RepoRevision { + evidenceLevel = EvidenceSupported + } + + // Basic operation check + if kind == "route_absent" && record.Operation != "repository_search" && record.Operation != "route_lookup" { + premiseStatus = PremiseInvalid + } + + if opts != nil && opts.RepoRoot != "" && record.Path != "" { + if err := ValidateEvidencePath(opts.RepoRoot, record.Path); err != nil { + premiseStatus = PremiseInvalid + } + if len(record.Anchors) > 0 { + targetPath := filepath.Join(opts.RepoRoot, filepath.Clean(record.Path)) + if err := CheckFileAnchors(targetPath, record.Anchors); err != nil { + premiseStatus = PremiseInvalid + } } } } } + + resolution := ResolvePlanDecision(PlanDecisionInput{ + DecisionKind: "architecture_fact", + IsMaterial: false, + RepositoryEvidence: evidence, + EvidenceLevel: evidenceLevel, + PremiseStatus: premiseStatus, + }) + + if resolution.Operator != OperatorInfer { + return fmt.Errorf("architecture fact %s requires %s (%s): %s", id, resolution.Operator, resolution.RuleID, resolution.Reason) + } } unknowns, _ := objectSlice(plan["architecture_unknowns"]) diff --git a/labs/12-product-engineering-loop/product-engineering-loop/plan_validation_test.go b/labs/12-product-engineering-loop/product-engineering-loop/plan_validation_test.go index bec88f80b..e9cdf0298 100644 --- a/labs/12-product-engineering-loop/product-engineering-loop/plan_validation_test.go +++ b/labs/12-product-engineering-loop/product-engineering-loop/plan_validation_test.go @@ -60,8 +60,8 @@ func TestValidatePlanV2FactMissingEvidenceID(t *testing.T) { opts := &ValidatePlanOptions{PlanPath: "plan.md", RepoRoot: ""} err := validateArchitectureGrounding(plan, opts) - if err == nil || !strings.Contains(err.Error(), "references unknown evidence ID") { - t.Fatalf("expected missing evidence error, got: %v", err) + if err == nil || !strings.Contains(err.Error(), "requires escalate") || !strings.Contains(err.Error(), "unresolved uncertainty requires escalation") { + t.Fatalf("expected escalate error due to absent evidence, got: %v", err) } }