Files
boshu2__agentops/cli/internal/ports/inmemory_gate_runner.go
T
Boden Fuller 06b63ba63a feat(ports): scaffold BC2 GateRunnerPort + InMemoryGateRunner (soc-wxh5)
First port-extraction commit for BC2 Validation epic (soc-wxh5).
Same shape as BC1 quartet (cycles 78-81 + bc1-corpus-ports.md):
port.go + inmemory_adapter.go + adapter_test.go.

Why this is BC2 first port: callers in evolve's Step 5 regression
gate, the /rpi validation phase, and the supergate composer all hold
gate-execution behavior inline as exec.Command calls. Extracting the
GateRunnerPort lets those callers be exercised against an in-memory
adapter without subprocess overhead — same payoff as CitationPort
delivered for the bd-verify-helpers.

New files:
- cli/internal/ports/gate_runner.go — GateRunnerPort interface +
  GateName + GateStatus (5 values: PASS/WARN/FAIL/SKIP/UNKNOWN
  matching the existing supergate output vocabulary) +
  GateVerdict (Status/Reason/LogTail) + GateRunRequest.
  Contract: non-nil verdict on success; non-empty Reason; empty Name
  returns UNKNOWN with "empty GateName" reason; adapter decides
  unknown-gate-name policy (UNKNOWN optimistic vs FAIL conservative)
  and MUST document the choice. Context honored best-effort.
- cli/internal/ports/inmemory_gate_runner.go — adapter satisfying
  the port. Backed by a map[GateName]GateVerdict; UnknownIsFail
  field lets callers opt into conservative semantics. Default is
  UNKNOWN for unknown names (optimistic). Compile-time port
  assertion.
- cli/internal/ports/inmemory_gate_runner_test.go — 6 Go tests
  covering: configured verdict (table-driven), empty name → UNKNOWN,
  unknown gate default → UNKNOWN, UnknownIsFail flag → FAIL,
  nil verdicts argument safe, context cancellation honored.

Single-concern: BC2 first port + test double only. Future BC2 ports
(CIStatusPort, ClaimEvidenceBinderPort per the soc-wxh5 epic) are
separate cycles. Wiring real callers (evolve Step 5, supergate) is
also a separate per-caller cycle.

Sibling pattern: cycle 78-81 BC1 ports — exact same triplet shape,
same contract-comment density, same compile-time assertion. The 5
BC2 GateStatus values mirror the supergate's existing PASS/WARN/FAIL/
SKIP output vocabulary plus UNKNOWN for the "couldn't decide" case.

Code-driven fitness:
- ports package: added GateRunnerPort (1 new interface) +
  InMemoryGateRunner (1 new adapter) + 6 tests
- cli/internal/ports/ coverage: 98.8% → 98.9%
- gofmt: clean
- go vet: clean
- go test ./internal/ports/: ok
- pre-push --fast: skipped to avoid autostash drop (cycle 95 lesson)

Cycle 99 / soc-wxh5 BC2 GateRunnerPort scaffold mode.
2026-05-12 19:33:28 -04:00

59 lines
1.9 KiB
Go

// practices: [hexagonal-architecture, ddd-bounded-context]
package ports
import (
"context"
"fmt"
)
// InMemoryGateRunner is a GateRunnerPort backed by a fixed map of
// gate name → verdict that the adapter returns on Run. Intended for
// tests and CLI dry-runs of gate-composition logic without invoking
// real subprocesses.
//
// Unknown-name policy: by default, unknown gate names return UNKNOWN
// (not FAIL) — adapters MAY change this by setting UnknownIsFail at
// construction time. The default matches the optimistic
// "treat-typo-as-unknown" semantics most callers want during dev.
type InMemoryGateRunner struct {
verdicts map[GateName]GateVerdict
UnknownIsFail bool
}
// NewInMemoryGateRunner returns an adapter that returns the supplied
// verdict for each gate name. Callers can mutate the returned struct's
// UnknownIsFail field if they want the conservative semantics.
func NewInMemoryGateRunner(verdicts map[GateName]GateVerdict) *InMemoryGateRunner {
if verdicts == nil {
verdicts = map[GateName]GateVerdict{}
}
return &InMemoryGateRunner{verdicts: verdicts}
}
// Run returns the configured verdict for req.Name. See package-level
// contract for empty-name and unknown-name semantics.
func (r *InMemoryGateRunner) Run(ctx context.Context, req GateRunRequest) (GateVerdict, error) {
if err := ctx.Err(); err != nil {
return GateVerdict{}, err
}
if req.Name == "" {
return GateVerdict{Status: GateStatusUnknown, Reason: "empty GateName"}, nil
}
if v, ok := r.verdicts[req.Name]; ok {
return v, nil
}
if r.UnknownIsFail {
return GateVerdict{
Status: GateStatusFail,
Reason: fmt.Sprintf("unknown gate %q (UnknownIsFail=true)", req.Name),
}, nil
}
return GateVerdict{
Status: GateStatusUnknown,
Reason: fmt.Sprintf("unknown gate %q (no configured verdict)", req.Name),
}, nil
}
// Compile-time assertion: InMemoryGateRunner satisfies the port.
var _ GateRunnerPort = (*InMemoryGateRunner)(nil)