17 KiB
Validation Refactor Implementation Plan
This plan implements the target state described in 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
Validatorbehavior that works overartifacts.ArtifactCandidate.
-
Remove
RawValidator,RawValidationRequest, andRawValidationResult. The newValidatorcontract covers the current raw validation use case directly. -
Define execution class metadata:
type ExecutionClass string const ( ExecutionClassDeterministic ExecutionClass = "deterministic" ExecutionClassLLMBacked ExecutionClass = "llm_backed" ) -
Define the new request shape around immutable module output. It should carry:
Stageas a string-compatible stage identifier;LaneID;ModuleKey;SourceandSourceID;SourceInput;SessionID;References;LLMClient;LLMProfile;Options;Metadata;Schema;Payload;- chunk provenance for extract validation;
Chunksfor chunk validation;- ordered
ExtractOutputsfor merge validation; MergeOutputfor normalize validation.
-
Define the result shape as one decision over the whole module output:
type ValidationResult struct { Approved bool ReasonCode string Message string DiagnosticArtifactPath string Warnings []Warning } -
Define
Validatoras:type Validator interface { Name() string ExecutionClass() ExecutionClass Validate(ctx context.Context, req ValidationRequest) (ValidationResult, error) } -
Update tests under
internal/framework/contractsso fake validators exercise the new contract and no longer reference artifact candidates. -
Keep
artifacts.ArtifactCandidateonly 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:
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.ValidatorRegistryinternals so it registers validator specs, not generic module specs. -
Add a
pipeline.ValidatorSpecwith 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.RawValidationRegistryafter 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:
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.ModuleCatalogandpipeline.Registriesto carry the chain mapping registry alongside module and validator registries. -
Add manifest types under
internal/core/artifactsfor resolved validator chain provenance. Use a top-level manifest field so chunk, extract, merge, and normalize chains can all be represented: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.Validatorsand 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:
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
validatorsas an active chain. Keep that field rejected with an error that directs users toextract.validators,merge.validators, ornormalize.validators. -
Update file-config parsing so the implementation can distinguish:
- validators omitted;
validators: [];validators: [ ... ].
-
Represent this in pipeline profiles with this explicit override type:
type ValidatorOverride struct { Set bool Validators []ModuleBinding } -
Validate configured validators during config validation:
- validator module key must be non-empty;
referencesare 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_profileis 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-profilebehavior:- 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_profilevalues, 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:
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
ValidatorRegistryat runtime in resolved order. - For each validator call, populate the new
contracts.ValidationRequestwith:- 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
RejectedOutputand 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.
- semantic rejection returns a
- Preserve current warning behavior:
- warnings from accepted attempts are promoted;
- warnings from discarded retry attempts are not promoted;
- warning-only validators return
Approved:truewith 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.jsonandmanifest.jsonshapes 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:
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.
- Key:
-
Add
internal/validators/generic/always_reject.- Key:
generic/always_reject. - Execution class: deterministic.
- Always returns rejected with stable reason code
always_reject.
- Key:
-
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.
- Key:
-
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.
- Key:
-
Make
github.com/santhosh-tekuri/jsonschema/v6a direct dependency for schema validation. -
Extend
contracts.ResponseSchemawith an in-memory-only field: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:
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.
- Key:
- 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.ValidateReffor source reference validation.
- Key:
- 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_sourcewarnings when a spell name is not found in the cited source text.
- Key:
- 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.goand 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_relatednessmust 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:
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
extractmodulednd/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:
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.mdso validation policy reflects central mappings and validator packages rather than module-owned validator chains. - Update
docs/config.mdto document stage-local validator overrides:- unset uses defaults;
- explicit empty disables validators;
- explicit non-empty replaces defaults in configured order.
- Update
docs/cli.mdto list production validators and describe validation profile behavior. - Update
docs/internal/pipeline.mdwith the implemented validator contract, chain mapping, retry behavior, and manifest provenance. - Update
docs/internal/modules.mdto remove stale claims that modules own validator defaults. - Update
docs/integrations/json-output.mdfor any manifest shape changes. - Update
docs/troubleshooting.mdwith 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.mdwith a concise completed note after all stages are implemented. - Update
docs/roadmap/validation.mdso it no longer describes completed work as future work. Keep only deferred validation work, if any remains.
Focused documentation checks:
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:
go test ./...
go vet ./...
go build ./cmd/notarius
Also run focused packages added by this plan:
go test ./internal/validators/...