# Validation Refactor Implementation Plan This plan implements the target state described in [validation.md](validation.md). Follow the stages in order. Do not skip the focused tests for a stage before moving to the next one. ## Stage 1: Replace Legacy Validator Contracts With One Raw Output Contract Goal: make the raw module-output validation boundary the only framework validator contract. Tasks: - In `internal/framework/contracts`, replace the legacy candidate-oriented validator contract with a module-output contract. - Remove these legacy types after updating callers: - `ValidationDecision`; - candidate-oriented `ValidationRequest`; - candidate-oriented `ValidationResult`; - legacy `Validator` behavior that works over `artifacts.ArtifactCandidate`. - Remove `RawValidator`, `RawValidationRequest`, and `RawValidationResult`. The new `Validator` contract covers the current raw validation use case directly. - Define execution class metadata: ```go type ExecutionClass string const ( ExecutionClassDeterministic ExecutionClass = "deterministic" ExecutionClassLLMBacked ExecutionClass = "llm_backed" ) ``` - Define the new request shape around immutable module output. It should carry: - `Stage` as a string-compatible stage identifier; - `LaneID`; - `ModuleKey`; - `Source` and `SourceID`; - `SourceInput`; - `SessionID`; - `References`; - `LLMClient`; - `LLMProfile`; - `Options`; - `Metadata`; - `Schema`; - `Payload`; - chunk provenance for extract validation; - `Chunks` for chunk validation; - ordered `ExtractOutputs` for merge validation; - `MergeOutput` for normalize validation. - Define the result shape as one decision over the whole module output: ```go type ValidationResult struct { Approved bool ReasonCode string Message string DiagnosticArtifactPath string Warnings []Warning } ``` - Define `Validator` as: ```go type Validator interface { Name() string ExecutionClass() ExecutionClass Validate(ctx context.Context, req ValidationRequest) (ValidationResult, error) } ``` - Update tests under `internal/framework/contracts` so fake validators exercise the new contract and no longer reference artifact candidates. - Keep `artifacts.ArtifactCandidate` only if other non-validation code still needs it. If it becomes unused after later stages, remove it in Stage 6. Design requirements: - Validators must not mutate request payloads, source documents, chunks, or upstream outputs. - Validator execution errors are framework errors. Validator semantic rejection is represented by `ValidationResult{Approved:false}`. - Empty validation chains still approve output by default. Focused tests: ```sh go test ./internal/framework/contracts go test ./internal/framework/validate ``` ## Stage 2: Add Validator Specs, Chain Mappings, And Manifest Provenance Goal: make validators centrally registered and make default chains explicit, reviewable, and auditable. Tasks: - Replace the current `pipeline.ValidatorRegistry` internals so it registers validator specs, not generic module specs. - Add a `pipeline.ValidatorSpec` with at least: - `Key`; - `ExecutionClass`. - Keep validator construction lazy through `ValidatorRegistry.Build(key)`. - Add `ValidatorRegistry.Spec(key)` and sorted registered-spec accessors for catalog and manifest use. - Remove `pipeline.RawValidationRegistry` after replacing its callers with the new chain registry. The new chain registry should be keyed by: - stage; - module key. - Add a production/default mapping type such as: ```go type ValidatorChainMapping struct { Stage ModuleStage Module string Validators []ModuleBinding } ``` - Add a chain registry with these behaviors: - duplicate stage/module mappings are rejected; - unknown stages are rejected; - empty default chains may be represented by absence of a mapping; - lookup returns a defensive copy. - Extend `pipeline.ModuleCatalog` and `pipeline.Registries` to carry the chain mapping registry alongside module and validator registries. - Add manifest types under `internal/core/artifacts` for resolved validator chain provenance. Use a top-level manifest field so chunk, extract, merge, and normalize chains can all be represented: ```go type ValidatorChainManifest struct { Stage string `json:"stage"` LaneID string `json:"lane_id,omitempty"` ModuleKey string `json:"module_key"` Validators []ValidatorManifest `json:"validators"` } type ValidatorManifest struct { Key string `json:"key"` ExecutionClass string `json:"execution_class"` } ``` - Remove `ArtifactLaneManifest.Validators` and update affected tests and JSON output documentation to use top-level validator-chain provenance instead. Design requirements: - The production chain mapping is central catalog policy, not module-owned behavior. - A resolved run manifest must record the exact chain used for each validation point, including explicit empty chains. Focused tests: ```sh go test ./internal/framework/pipeline go test ./internal/core/artifacts ``` ## Stage 3: Implement Stage-Scoped Config Overrides Goal: allow pipeline config to override default validator mappings for each validatable stage while preserving unset versus explicit empty semantics. Tasks: - Add stage-local validator override syntax to module bindings for only: - `chunk`; - artifact lane `extract`; - artifact lane `merge`; - artifact lane `normalize`. - Do not revive artifact-lane-level `validators` as an active chain. Keep that field rejected with an error that directs users to `extract.validators`, `merge.validators`, or `normalize.validators`. - Update file-config parsing so the implementation can distinguish: - validators omitted; - `validators: []`; - `validators: [ ... ]`. - Represent this in pipeline profiles with this explicit override type: ```go type ValidatorOverride struct { Set bool Validators []ModuleBinding } ``` - Validate configured validators during config validation: - validator module key must be non-empty; - `references` are not supported on validator bindings; - nested validator overrides inside validator bindings are not supported; - retries are not supported on validator bindings in this pass; - explicit `llm_profile` is allowed only for LLM-backed validators and is rejected for deterministic validators during resolution. - During pipeline resolution: - unset override uses the central default chain; - explicit empty override resolves to an empty chain and approves by default; - explicit non-empty override resolves exactly the configured validators in configured order; - every referenced validator key must exist in the validator registry; - configured order must not be silently reordered. - Update `--llm-profile` behavior: - continue overriding chunk, extract, merge, and normalize module bindings; - do not override validator bindings unless validator-specific profile override behavior is explicitly added in a later roadmap. - Update Scriptorium explicit profile validation so it includes LLM-backed validators with explicit `llm_profile` values, and ignores deterministic validators. Design requirements: - Validator compatibility with a module or stage is not enforced during config resolution. - Empty resolved chains are valid and should be recorded in manifest provenance. - Config validation must fail before runtime if a non-empty override references an unknown validator. Focused tests: ```sh go test ./internal/core/config go test ./internal/framework/pipeline go test ./internal/cli ``` ## Stage 4: Wire Validator Execution Into The Runner Goal: run resolved validator chains at chunk, extract, merge, and normalize validation points. Tasks: - Replace runner calls to the old raw validation registry with calls to the resolved validator chain for the current validation point. - Build validators from `ValidatorRegistry` at runtime in resolved order. - For each validator call, populate the new `contracts.ValidationRequest` with: - immutable module output payload; - stage/module/lane/source/chunk provenance; - schema metadata and in-memory schema content when available; - source input material; - session ID; - resolved references for the target stage; - LLM client/profile/options/metadata for the validator; - upstream outputs needed by merge and normalize validators. - Preserve current retry behavior: - semantic rejection returns a `RejectedOutput` and may be retried according to the module binding's retry setting; - validator execution errors are framework errors and are retried under the same module binding retry setting; - final rejection is recorded and does not pass downstream. - Preserve current warning behavior: - warnings from accepted attempts are promoted; - warnings from discarded retry attempts are not promoted; - warning-only validators return `Approved:true` with warnings. - Continue enforcing framework-level chunk invariants in the runner before chunk validators run. - Populate manifest validator-chain provenance after resolution and before output encoding. - Keep rejected output manifest entries compatible with current `rejected.json` and `manifest.json` shapes where practical. Design requirements: - Validators are read-only. Do not let a validator return rewritten payloads or replacement stage outputs. - If a validator chain is empty, approve without building validators. - If a validator is mapped to an unsuitable output shape, the validator should return a clear execution error or an explicit documented approval. Focused tests: ```sh go test ./internal/framework/pipeline go test ./internal/cli ``` ## Stage 5: Add Generic Validators Goal: provide reusable validators needed for incremental module development and production structural checks. Tasks: - Add `internal/validators/generic/always_accept`. - Key: `generic/always_accept`. - Execution class: deterministic. - Always returns approved. - Add `internal/validators/generic/always_reject`. - Key: `generic/always_reject`. - Execution class: deterministic. - Always returns rejected with stable reason code `always_reject`. - Add `internal/validators/generic/valid_json`. - Key: `generic/valid_json`. - Execution class: deterministic. - Accepts syntactically valid JSON. - Rejects invalid JSON with stable reason code `invalid_json`. - Add `internal/validators/generic/valid_json_schema`. - Key: `generic/valid_json_schema`. - Execution class: deterministic. - Uses the module output's in-memory JSON schema content. - Rejects invalid JSON with `invalid_json`. - Rejects schema non-conformance with `json_schema_invalid`. - Returns a validator execution error when no schema content is available. - Make `github.com/santhosh-tekuri/jsonschema/v6` a direct dependency for schema validation. - Extend `contracts.ResponseSchema` with an in-memory-only field: ```go JSONSchema []byte `json:"-"` ``` Module outputs should carry schema bytes to validators without serializing raw schema content into manifests, diagnostics, or default output files. - Update D&D scene and D&D spell modules to populate in-memory schema content on their output schema values. - Do not add a shared validator test helper package in this pass. Keep tests local unless a later cleanup finds substantial duplication. Design requirements: - Generic validators must not depend on concrete production modules. - Tests must not assert exact embedded production prompt text. - Raw schema content must not be emitted in diagnostics or manifests. Focused tests: ```sh go test ./internal/validators/generic/... go test ./internal/modules/chunk/dnd/scenes go test ./internal/modules/extract/dnd/spells go test ./internal/framework/pipeline ``` ## Stage 6: Migrate D&D Spell Validators Goal: move D&D spell validation behavior out of the extractor package and into raw-output validators. Tasks: - Add `internal/validators/extract/dnd/spells/shape`. - Key: `extract/dnd/spells/shape`. - Execution class: deterministic. - Parses raw spell JSON and rejects malformed spell-cast payloads or missing required fields. - Add `internal/validators/extract/dnd/spells/source_refs`. - Key: `extract/dnd/spells/source_refs`. - Execution class: deterministic. - Parses raw spell JSON and rejects missing or invalid source references. - Use `internal/core/source.ValidateRef` for source reference validation. - Add `internal/validators/extract/dnd/spells/source_relatedness`. - Key: `extract/dnd/spells/source_relatedness`. - Execution class: deterministic. - Warning-only validator. - Approves output and emits `spell_not_near_source` warnings when a spell name is not found in the cited source text. - Keep D&D-specific validator parsing local to validator packages or a validator helper package. Do not import concrete extractor packages from validators. - Remove `internal/modules/extract/dnd/spells/validator.go` and its candidate-oriented tests after equivalent validator-package tests exist. - Remove now-unused legacy candidate validation helpers and artifact candidate types if they have no remaining callers. Design requirements: - The spell extractor should remain responsible for prompt/schema/provenance and raw output production, not accept/reject policy. - `source_relatedness` must not reject output. It should only warn. - The default D&D spell chain must preserve the intended order: `generic/valid_json`, `generic/valid_json_schema`, `extract/dnd/spells/shape`, `extract/dnd/spells/source_refs`, `extract/dnd/spells/source_relatedness`. Focused tests: ```sh go test ./internal/validators/extract/dnd/spells/... go test ./internal/modules/extract/dnd/spells go test ./internal/framework/pipeline ``` ## Stage 7: Register Production Validators And Defaults Goal: make production validation policy explicit and active. Tasks: - Register all generic validators in `internal/cli/catalog.go`. - Register all D&D spell validators in `internal/cli/catalog.go`. - Register default production chain mappings centrally near production module registration. - Add the default chain for `extract` module `dnd/spells`: - `generic/valid_json`; - `generic/valid_json_schema`; - `extract/dnd/spells/shape`; - `extract/dnd/spells/source_refs`; - `extract/dnd/spells/source_relatedness`. - Do not add default chains for other modules unless the stage has a meaningful validator for that output shape. - Update catalog construction tests so available modules, available validators, and default mappings can be reviewed together. - Add CLI tests proving: - default D&D spell validators run; - explicit empty override disables the default chain; - explicit non-empty override replaces the default chain; - configured validator order is preserved; - unknown configured validator keys fail during validation/resolution; - deterministic validators reject explicit `llm_profile`; - LLM-backed validator profile validation checks explicit profile IDs. Design requirements: - Production defaults must be reviewable without constructing concrete modules. - Empty default chains approve by default. - The manifest records resolved chains for default chains, empty overrides, and explicit override chains. Focused tests: ```sh go test ./internal/cli go test ./internal/core/config go test ./internal/framework/pipeline ``` ## Stage 8: Documentation, Examples, And Cleanup Goal: make implemented validation behavior canonical outside roadmap docs and remove stale roadmap instructions. Tasks: - Update `docs/policy/architecture.md` so validation policy reflects central mappings and validator packages rather than module-owned validator chains. - Update `docs/config.md` to document stage-local validator overrides: - unset uses defaults; - explicit empty disables validators; - explicit non-empty replaces defaults in configured order. - Update `docs/cli.md` to list production validators and describe validation profile behavior. - Update `docs/internal/pipeline.md` with the implemented validator contract, chain mapping, retry behavior, and manifest provenance. - Update `docs/internal/modules.md` to remove stale claims that modules own validator defaults. - Update `docs/integrations/json-output.md` for any manifest shape changes. - Update `docs/troubleshooting.md` with validator-chain debugging guidance. - Leave maintained example configs unchanged unless a validator override example is added intentionally with corresponding CLI/config tests. Keep examples secret-free and loadable. - Replace `docs/roadmap/implementation.md` with a concise completed note after all stages are implemented. - Update `docs/roadmap/validation.md` so it no longer describes completed work as future work. Keep only deferred validation work, if any remains. Focused documentation checks: ```sh rg -n "reserved for future configurable validator chains|configured validators are not supported|candidate-oriented|RawValidationRegistry" docs internal ``` Expected result after implementation: no stale current-behavior documentation describes configured validators as rejected, and no production code path depends on the legacy candidate-oriented validator contract. ## Full Validation Run after every stage that changes shared contracts, and at the end: ```sh go test ./... go vet ./... go build ./cmd/notarius ``` Also run focused packages added by this plan: ```sh go test ./internal/validators/... ```