From 1e122f047317e28927206cf3d081b4c3c9d40ecf Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 5 Nov 2026 13:54:06 +0000 Subject: [PATCH] Require validation preparation internally --- docs/roadmap/implementation.md | 4 ++++ internal/engine/engine.go | 2 +- internal/usecase/prepared_execution_test.go | 10 +++++----- internal/usecase/runner.go | 12 ++++-------- internal/usecase/smoke_inference_case.go | 6 +----- internal/usecase/smoke_selection_test.go | 5 +++-- internal/validate/cancellation_test.go | 6 +++--- internal/validate/standard_validator.go | 4 ++-- internal/validate/standard_validator_test.go | 12 ++---------- 9 files changed, 25 insertions(+), 36 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d19dc1d..a254c01 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -288,6 +288,10 @@ runner constructors, and their test doubles. compatibility adapters. Run applicable validation/preparation tests when this signature change is wired; temporary compilation gaps may continue into Stage 11. +Completion: runner and engine validation dependencies now require preparation; +concrete constructors retain direct validation, and nil-plan behavior is tested. +Validation, use-case, and engine tests pass on Go 1.27.3. + ## Stage 11: Share Root Loading And Schema Compilation **Catalog:** 6, 21, compilation part. **Files:** `internal/validate` implementations. diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 03472db..3369564 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -43,7 +43,7 @@ type ConfigurationSources struct { type Options struct { llmClient llm.Client artifactReader artifact.Reader - validator validate.Validator + validator validate.ValidationPreparer validatorSource bool artifactSource bool disableBundledOpenRouter bool diff --git a/internal/usecase/prepared_execution_test.go b/internal/usecase/prepared_execution_test.go index d7d8430..e423151 100644 --- a/internal/usecase/prepared_execution_test.go +++ b/internal/usecase/prepared_execution_test.go @@ -53,10 +53,10 @@ type recordingValidationPreparer struct { directValidateCalls int } -type validationOnly struct{} +type nilPlanPreparer struct{} -func (validationOnly) Validate(context.Context, *domain.Artifact, domain.OutputContract) (domain.ValidationResult, error) { - return domain.ValidationResult{}, nil +func (nilPlanPreparer) PrepareValidation(context.Context, domain.OutputContract) (validate.PreparedValidation, error) { + return nil, nil } func (v *recordingValidationPreparer) Validate( @@ -791,7 +791,7 @@ func TestRunnerPreparedExecutionWithoutValidatorSkipsValidation(t *testing.T) { } } -func TestRunnerPrepareExecutionRequiresValidationPreparer(t *testing.T) { +func TestRunnerPrepareExecutionRejectsNilValidationPlan(t *testing.T) { reader := defaultArtifactReader() runner := NewRunner( &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationBasic, 0)}, @@ -800,7 +800,7 @@ func TestRunnerPrepareExecutionRequiresValidationPreparer(t *testing.T) { reader, defaultRenderer(), &fakeLLM{forbid: true}, - validationOnly{}, + nilPlanPreparer{}, nil, ) diff --git a/internal/usecase/runner.go b/internal/usecase/runner.go index 9092a64..316de9d 100644 --- a/internal/usecase/runner.go +++ b/internal/usecase/runner.go @@ -49,7 +49,7 @@ type Runner struct { artifacts artifact.Reader renderer prompt.Renderer llm llm.Client - validator validate.Validator + validator validate.ValidationPreparer repairer OutputRepairer admitter RunAdmitter defaultTransientRetries uint64 @@ -87,7 +87,7 @@ func NewRunner( artifacts artifact.Reader, renderer prompt.Renderer, llmClient llm.Client, - validator validate.Validator, + validator validate.ValidationPreparer, admitter RunAdmitter, ) *Runner { return NewRunnerWithRepairer( @@ -108,7 +108,7 @@ func NewRunnerWithRepairer( artifacts artifact.Reader, renderer prompt.Renderer, llmClient llm.Client, - validator validate.Validator, + validator validate.ValidationPreparer, repairer OutputRepairer, admitter RunAdmitter, defaultTransientRetries ...uint64, @@ -409,11 +409,7 @@ func (r *Runner) prepareValidation( if r.validator == nil { return noOpPreparedValidation{contract: contract}, nil } - preparer, ok := r.validator.(validate.ValidationPreparer) - if !ok { - return nil, fmt.Errorf("%w: validator does not support prepared validation", ErrValidation) - } - plan, err := preparer.PrepareValidation(ctx, contract) + plan, err := r.validator.PrepareValidation(ctx, contract) if err != nil { return nil, &OperationError{Category: ErrValidation, Cause: err} } diff --git a/internal/usecase/smoke_inference_case.go b/internal/usecase/smoke_inference_case.go index a56072b..6936e5b 100644 --- a/internal/usecase/smoke_inference_case.go +++ b/internal/usecase/smoke_inference_case.go @@ -52,11 +52,7 @@ func prepareSmokeInferenceCase( }, nil case domain.SmokeTestStructuredOutput: validator := validate.NewFSValidator(smokeSchemaFS, ".") - preparer, ok := validator.(validate.ValidationPreparer) - if !ok { - return nil, fmt.Errorf("%w: private validator does not support prepared validation", ErrValidation) - } - plan, err := preparer.PrepareValidation(ctx, domain.OutputContract{ + plan, err := validator.PrepareValidation(ctx, domain.OutputContract{ Format: domain.FormatJSON, ValidationMode: domain.ValidationJSONSchema, SchemaPath: smokeStructuredSchemaPath, diff --git a/internal/usecase/smoke_selection_test.go b/internal/usecase/smoke_selection_test.go index c9e0613..0901ad7 100644 --- a/internal/usecase/smoke_selection_test.go +++ b/internal/usecase/smoke_selection_test.go @@ -9,6 +9,7 @@ import ( "go.maximumdirect.net/promptkit/internal/configuration" "go.maximumdirect.net/promptkit/internal/domain" "go.maximumdirect.net/promptkit/internal/profile" + "go.maximumdirect.net/promptkit/internal/validate" ) type selectionTestCollaborators struct { @@ -41,9 +42,9 @@ func (c selectionTestLLM) Generate(context.Context, domain.GenerateRequest) (*do type selectionTestValidator struct{ state *selectionTestCollaborators } -func (v selectionTestValidator) Validate(context.Context, *domain.Artifact, domain.OutputContract) (domain.ValidationResult, error) { +func (v selectionTestValidator) PrepareValidation(context.Context, domain.OutputContract) (validate.PreparedValidation, error) { v.state.validationCalls++ - return domain.ValidationResult{}, errors.New("validation must not occur during target selection") + return nil, errors.New("validation must not occur during target selection") } type selectionTestRepositories struct { diff --git a/internal/validate/cancellation_test.go b/internal/validate/cancellation_test.go index 16d74c3..8a0408d 100644 --- a/internal/validate/cancellation_test.go +++ b/internal/validate/cancellation_test.go @@ -19,7 +19,7 @@ func TestValidationCancellationBeforeWorkDoesNotOpenSchemaSource(t *testing.T) { source := &countingSchemaFS{FS: fstest.MapFS{ "schema.json": {Data: []byte(`{"type":"object"}`)}, }} - validator := NewFSValidator(source, ".").(ValidationPreparer) + validator := NewFSValidator(source, ".") ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -45,7 +45,7 @@ func TestValidationCancellationBetweenReferencedSchemaReadChunks(t *testing.T) { target: "child.json", cancel: cancel, } - validator := NewFSValidator(source, ".").(ValidationPreparer) + validator := NewFSValidator(source, ".") plan, err := validator.PrepareValidation(ctx, domain.OutputContract{ ValidationMode: domain.ValidationJSONSchema, @@ -113,7 +113,7 @@ func TestCancellationDoesNotDetachBlockedSchemaRead(t *testing.T) { started: started, release: release, } - validator := NewFSValidator(source, ".").(ValidationPreparer) + validator := NewFSValidator(source, ".") type outcome struct { plan PreparedValidation err error diff --git a/internal/validate/standard_validator.go b/internal/validate/standard_validator.go index f23c530..fe3e26c 100644 --- a/internal/validate/standard_validator.go +++ b/internal/validate/standard_validator.go @@ -34,11 +34,11 @@ type FSValidator struct { root string } -func NewStandardValidator(schemaBaseDir string) Validator { +func NewStandardValidator(schemaBaseDir string) *StandardValidator { return &StandardValidator{schemaBaseDir: schemaBaseDir} } -func NewFSValidator(fsys fs.FS, root string) Validator { +func NewFSValidator(fsys fs.FS, root string) *FSValidator { return &FSValidator{fsys: fsys, root: root} } diff --git a/internal/validate/standard_validator_test.go b/internal/validate/standard_validator_test.go index 7561432..159487f 100644 --- a/internal/validate/standard_validator_test.go +++ b/internal/validate/standard_validator_test.go @@ -250,11 +250,7 @@ func TestStandardValidatorPreparedSchemaSurvivesSourceRemoval(t *testing.T) { } validator := NewStandardValidator(tmp) - preparer, ok := validator.(ValidationPreparer) - if !ok { - t.Fatal("standard validator does not support validation preparation") - } - prepared, err := preparer.PrepareValidation(context.Background(), domain.OutputContract{ + prepared, err := validator.PrepareValidation(context.Background(), domain.OutputContract{ ValidationMode: domain.ValidationJSONSchema, SchemaPath: "schema.json", }) @@ -432,11 +428,7 @@ func TestFSValidatorPreparedSchemaSurvivesSourceMutation(t *testing.T) { }`)}, } validator := NewFSValidator(fsys, ".") - preparer, ok := validator.(ValidationPreparer) - if !ok { - t.Fatal("filesystem validator does not support validation preparation") - } - prepared, err := preparer.PrepareValidation(context.Background(), domain.OutputContract{ + prepared, err := validator.PrepareValidation(context.Background(), domain.OutputContract{ ValidationMode: domain.ValidationJSONSchema, SchemaPath: "schema.json", })