Files
Maven/docs/reports/2026-09-05-slice3-summary.md
T

6.4 KiB

Slice 3: Structural validation between ActionCandidate resolution and execution

Date: 2026-09-05 Base: 6a402bf (slice 2 committed) Status: complete

Files changed

File Lines added
internal/router/actioncandidate.go +76 (types + ValidateActionCandidate)
internal/router/actioncandidate_test.go +98 (6 unit tests)
cmd/mavend/actions_act.go +11 (validation gate in actionAct)
cmd/mavend/actionresolve.go +37 (noteActionValidation tracing)
cmd/mavend/actionresolve_test.go +128 (5 integration tests)
docs/reports/2026-09-05-slice3-structural-validation.md full report

Validation contract

type ActionValidationResult struct {
    Unresolved bool
    Valid      bool
    Issues     []ActionValidationIssue
}

type ActionValidationIssue struct {
    Field  ActionField
    Reason string
}

type ActionField string
const (
    FieldFn   ActionField = "fn"
    FieldArgs ActionField = "args"
)

Three disjoint outcomes:

  • Unresolved: Fn is empty. Not a validation error. Routes to proposeGap / clarification.
  • Valid: Fn non-empty, structurally admissible. Proceeds to risk policy and execution.
  • Invalid: Fn non-empty but malformed. Refused with ActFail.

Exact validation rules

Rule Check Outcome on fail
unresolved c.Fn == "" Unresolved (not invalid)
blank_function_name strings.TrimSpace(c.Fn) == "" when Fn is non-empty Invalid

Before/after action flow

Before (slice 2):

RouteDecision → ResolveActionCandidate → ActionCandidate
  → actionAct writes Fn/Args back into dec.Slots
  → refusesCommand check
  → task-status intercept
  → Praxis intercept
  → Hexis intercept
  → !HasFn → proposeGap
  → tool.Executor.Exec → error switch

After (slice 3):

RouteDecision → ResolveActionCandidate → ActionCandidate
  → ValidateActionCandidate
    ├─ unresolved → (continues to existing flow; !HasFn → proposeGap)
    ├─ invalid → ActFail (early return)
    └─ valid → (continues)
  → actionAct writes Fn/Args back into dec.Slots
  → refusesCommand check
  → task-status intercept
  → Praxis intercept
  → Hexis intercept
  → !HasFn → proposeGap
  → tool.Executor.Exec → error switch

How unresolved differs from invalid

Unresolved Invalid
Fn empty ("") non-empty but malformed (e.g. whitespace-only)
Cause Matcher miss, non-act intent Structural defect in route/matcher output
Trace action-validation:declined:unresolved action-validation:declined:invalid:<reason>
Response proposeGap (existing proposal/scaffold path) ActFail
Execution does not reach tool executor does not reach tool executor

Tests added

Unit tests (internal/router)

Test Pins
TestValidateActionCandidate_UnresolvedEmptyFn Empty Fn → unresolved, not invalid
TestValidateActionCandidate_UnresolvedMatcherMiss Matcher miss candidate → unresolved
TestValidateActionCandidate_ValidRoute Route-resolved candidate → valid
TestValidateActionCandidate_ValidMatcher Matcher-resolved candidate → valid
TestValidateActionCandidate_ValidNoArgs Zero-arg tool → valid
TestValidateActionCandidate_BlankFn Whitespace-only Fn → invalid with blank_function_name issue

Integration tests (cmd/mavend)

Test Pins
TestActValidation_MalformedCandidate_BlankFn Blank Fn does not execute, produces response
TestActValidation_UnresolvedCandidate_ProposeGap Matcher miss still flows to proposeGap
TestActValidation_DestructiveValid_StillConfirms Destructive valid act still triggers confirm turn
TestActValidation_IrreversibleValid_NeedsAuthedSurface Irreversible valid act still reaches authed-surface refusal
TestActValidation_ValidationTracing Validation outcome recorded in decision trace

Existing regression tests preserved (all pass)

Test What it pins
TestActRouteSource_NoMatcherInvoke Route-resolved act executes, matcher not invoked
TestActMatcherSource_FallbackMatch Matcher-resolved act executes
TestActMatcherMiss_ProposeGap Matcher miss → proposeGap
TestActDestructive_ConfirmationUnchanged Destructive → confirm turn
TestActTaskStatus_InterceptUnchanged task_status intercepted before tool execution
TestActStage0_SameResult Stage-0 grammar act executes
TestActLearnedRouter_NoFn_FallbackMatch LLM-routed act without Fn → matcher fallback
TestActPathSpeaksTheTiers Risk tiers spoken correctly
TestActionActMarkerReferentMovesOnlyTheNamedStoredTask Task-status move unchanged
TestActOffAllowlistIsStillRefused Off-allowlist act refused

Test results

internal/router: PASS (1.1s) — 13 actioncandidate tests (7 resolve + 6 validate)
cmd/mavend:      PASS (23.4s) — 21 act-path tests (15 existing + 5 new + 1 tracing)

Risk/confirmation/execution behavior unchanged

  • Risk tiers: tool.RiskOf and tool.PolicyFor not touched. Validation happens before risk policy.
  • Confirmation: ErrNeedsConfirm → confirm turn unchanged.
  • Irreversible: ErrNeedsAuthedSurface → refusal unchanged.
  • Execution: tool.Executor.Exec not modified. Validation is a pure pre-screen.
  • Ecosystem intercepts: Praxis and Hexis intercepts unchanged.

Remaining downstream consumers of Decision.Slots

Consumer File Reads
resolveTaskStatus cmd/mavend/actions_task.go:109 dec.Slots.Fn
handlePraxisAct cmd/mavend/ecosystem_acts.go:110 dec.Slots.HasFn, dec.Slots.Fn, dec.Slots.Args
handleHexisAct cmd/mavend/ecosystem_acts.go:660 via ActHasEntityTarget(dec)
proposeGap cmd/mavend/confirm.go:191 dec.Utterance (not Slots)
tool.Executor.Exec internal/tool/tool.go:156 called with dec.Slots.Fn, dec.Slots.Args
actPhrase cmd/mavend/actions_act.go dec.Slots.Fn, dec.Slots.Args
park cmd/mavend/confirm.go dec.Slots.Fn, dec.Slots.Args

Can ActionCandidate become authoritative?

Yes. The bridge write is the only coupling. In a later slice, downstream consumers can read directly from the candidate. No semantic changes required — mechanical refactoring of argument passing.