mavend: centralize action validation boundary (slice 4)

This commit is contained in:
2026-09-06 12:53:54 +04:00
parent 6a402bf556
commit 356766bce1
32 changed files with 2061 additions and 178 deletions
+76
View File
@@ -1,5 +1,7 @@
package router
import "strings"
// ActionCandidate — the result of action resolution, produced before execution.
// It replaces the implicit ownership split where the router filled Slots.Fn/Args
// and actionAct re-matched when they were absent. One candidate is produced per
@@ -44,6 +46,41 @@ const (
// ActionResolved reports whether the candidate resolved to a function.
func (c ActionCandidate) ActionResolved() bool { return c.Fn != "" }
// --- structural validation ---
// ActionField identifies a structural field of ActionCandidate for validation
// reporting. Kept as a string enum so callers can switch on known values
// without importing a large set.
type ActionField string
const (
FieldFn ActionField = "fn"
FieldArgs ActionField = "args"
)
// ActionValidationIssue records one structural problem found by
// ValidateActionCandidate. Field names which field; Reason is a short
// machine-readable tag, not a human sentence.
type ActionValidationIssue struct {
Field ActionField
Reason string
}
// ActionValidationResult is the typed output of ValidateActionCandidate.
// Three disjoint outcomes:
//
// - Unresolved: Fn is empty — the matcher did not match. The caller should
// route to proposeGap / clarification, not treat this as a structural error.
// - Valid: Fn is non-empty and the candidate is structurally admissible for
// the current execution path.
// - Invalid: Fn is non-empty but the candidate is structurally malformed.
// The caller should refuse execution.
type ActionValidationResult struct {
Unresolved bool
Valid bool
Issues []ActionValidationIssue
}
// ResolveActionCandidate produces an ActionCandidate from a routing decision.
// It is the single boundary between routing and action resolution: everything
// downstream consumes the candidate rather than re-resolving the function.
@@ -93,3 +130,42 @@ func ResolveActionCandidate(dec Decision, m ActMatcher) ActionCandidate {
Confidence: dec.Confidence,
}
}
// ValidateActionCandidate checks whether a resolved ActionCandidate is
// structurally admissible for the current execution path. It answers only
// shape/completeness — not trust, confidence, risk, or semantic correctness.
//
// Three outcomes:
// - Unresolved (Fn empty): the matcher missed. Caller routes to
// proposeGap / clarification. This is NOT a validation error.
// - Valid (Fn non-empty, structure sound): candidate may proceed to
// risk policy and execution.
// - Invalid (Fn non-empty but malformed): candidate must not execute.
//
// Validation does not re-run routing, intent classification, matcher
// resolution, language parsing, or entity inference.
func ValidateActionCandidate(c ActionCandidate) ActionValidationResult {
// Unresolved: Fn is empty. The matcher did not match, or the decision
// was not an act. This flows into the existing propose-gap / proposal
// path and must NOT be treated as a structural error.
if c.Fn == "" {
return ActionValidationResult{Unresolved: true}
}
var issues []ActionValidationIssue
// Fn is present but must be a non-blank identifier. The route and
// matcher both produce bare function names; whitespace-only or
// control-character Fn would be a structural defect in the producer.
if strings.TrimSpace(c.Fn) == "" {
issues = append(issues, ActionValidationIssue{
Field: FieldFn,
Reason: "blank_function_name",
})
}
if len(issues) > 0 {
return ActionValidationResult{Issues: issues}
}
return ActionValidationResult{Valid: true}
}