421 lines
18 KiB
Markdown
421 lines
18 KiB
Markdown
# Validation System Refactor
|
|
|
|
This roadmap defines the target state for making validation a first-class,
|
|
composable pipeline concern. Current validation behavior is partly module-owned:
|
|
the `dnd/spells` extractor defines built-in validators inside the module package,
|
|
and the runner falls back to extractor-provided validators when a lane does not
|
|
configure validators. The desired end state is that validator implementations,
|
|
validator registration, and default module-to-validator mappings are explicit,
|
|
reviewable, and independent of concrete module packages.
|
|
|
|
## Goals
|
|
|
|
- Move artifact and module-output validation behavior out of `internal/modules`
|
|
and into `internal/validators`.
|
|
- Keep each validator in its own package.
|
|
- Mirror the stage and domain shape of `internal/modules` where a validator is
|
|
module-specific.
|
|
- Support deterministic and LLM-backed validators through the same framework
|
|
contract.
|
|
- Allow validators to be mapped to modules at any pipeline stage that returns
|
|
module output for validation: `chunk`, `extract`, `merge`, or `normalize`.
|
|
- Make default production module-to-validator mappings centralized and
|
|
human-readable.
|
|
- Allow pipeline configuration to override default mappings for advanced use.
|
|
- Treat an empty validator set as valid and equivalent to approval.
|
|
- Preserve the rule that module output passes forward unless a validator rejects
|
|
it.
|
|
- Make successfully returned module output the explicit validation boundary:
|
|
questions about output syntax, media type, schema conformance, and domain
|
|
acceptability should be answered by validators.
|
|
|
|
## Non-Goals
|
|
|
|
- Do not create a general workflow engine or arbitrary validation DAG.
|
|
- Do not enforce validator compatibility with a module or stage in this pass.
|
|
- Do not move ordinary runtime invariant checks into validator packages.
|
|
- Do not require every module to have validators.
|
|
- Do not require LLM-backed validators for modules that can be checked
|
|
deterministically.
|
|
- Do not silently reorder configured validator chains unless that behavior is
|
|
introduced deliberately and documented as part of the validator contract.
|
|
|
|
## Validation Boundary
|
|
|
|
Validation packages should own approve/reject/warning evaluation of successfully
|
|
returned module outputs. This means logic that decides whether a chunk result,
|
|
raw extract output, raw merge output, raw normalize output, or raw LLM response
|
|
should continue through the pipeline belongs in `internal/validators`.
|
|
|
|
The boundary is:
|
|
|
|
- no module output was returned: execution failed, and the pipeline should report
|
|
a module or runtime error;
|
|
- module output was returned: the validator chain decides whether that output is
|
|
acceptable, and an empty validator chain approves it.
|
|
|
|
Scriptorium and provider errors are execution failures rather than validator
|
|
rejections. This includes provider timeouts, authentication failures,
|
|
transport/runtime failures, Scriptorium structured-output retry exhaustion, and
|
|
malformed responses that Scriptorium rejects before returning module output.
|
|
|
|
Other validation-like checks should remain with their owning packages:
|
|
|
|
- input parsing and source-format validation stay in input modules;
|
|
- source document and source reference invariants stay in `internal/core/source`;
|
|
- config validation stays in `internal/core/config`;
|
|
- registry, profile, and pipeline consistency checks stay in framework and CLI
|
|
code;
|
|
- response schema loading stays in module asset code;
|
|
- Scriptorium runtime errors stay in LLM runtime code.
|
|
|
|
Domain validators may call reusable core helpers such as `source.ValidateRef`,
|
|
but the module-output approval or rejection decision should be made by a
|
|
validator.
|
|
|
|
Validators should answer module-output questions such as:
|
|
|
|
- is returned content syntactically valid JSON;
|
|
- does returned JSON conform to the module's declared schema;
|
|
- are required domain fields present and non-empty;
|
|
- are source references valid and appropriately grounded;
|
|
- does domain-specific output satisfy the configured policy.
|
|
|
|
## Audita Patterns To Adapt
|
|
|
|
The validator architecture should adapt useful patterns from
|
|
[`audita`](https://gitea.maximumdirect.net/eric/audita) without copying its
|
|
narrower transcript-correction shape directly.
|
|
|
|
Useful patterns:
|
|
|
|
- concrete validators live under `internal/validators`;
|
|
- shared validator runtime mechanics live under a framework package;
|
|
- built-in validator keys are stable and centrally registered;
|
|
- built-in chains are centrally reviewable;
|
|
- validators carry execution-class metadata;
|
|
- deterministic and LLM-backed validators implement one contract;
|
|
- LLM-backed validator runtime can share batching, diagnostics, structured
|
|
response handling, and malformed-response policy;
|
|
- reports and manifests can classify validator decisions by execution class.
|
|
|
|
Important Notarius differences:
|
|
|
|
- mappings must be keyed by stage and module key, not module key alone;
|
|
- mappings should be owned by the central production catalog, not resolved inside
|
|
concrete module constructors;
|
|
- configured mapping order should be authoritative unless the config explicitly
|
|
opts into a different ordering policy;
|
|
- validators must support chunk, extract, merge, and normalize outputs rather
|
|
than only one proposal shape.
|
|
|
|
## Validator Package Layout
|
|
|
|
Concrete validators should live under `internal/validators`. Module-specific
|
|
validators should mirror the module tree and use one package per validator:
|
|
|
|
```text
|
|
internal/validators/extract/dnd/spells/shape
|
|
internal/validators/extract/dnd/spells/source_refs
|
|
internal/validators/extract/dnd/spells/source_relatedness
|
|
```
|
|
|
|
Generic validators may live under stage-specific generic paths when they operate
|
|
on a particular stage output shape:
|
|
|
|
```text
|
|
internal/validators/chunk/generic/...
|
|
internal/validators/extract/generic/...
|
|
internal/validators/merge/generic/...
|
|
internal/validators/normalize/generic/...
|
|
```
|
|
|
|
Truly stage-independent validators may live under `internal/validators/generic`
|
|
once there is a real shared validator that justifies that location. Generic JSON
|
|
syntax and JSON schema validators are likely candidates for
|
|
`internal/validators/generic/valid_json` and
|
|
`internal/validators/generic/valid_json_schema`.
|
|
|
|
Each validator package should expose:
|
|
|
|
- a stable validator key;
|
|
- execution-class metadata;
|
|
- a constructor;
|
|
- a validator spec suitable for registration;
|
|
- a `Register` function;
|
|
- focused tests for decisions, warnings, errors, and diagnostics behavior.
|
|
|
|
Reusable validator runtime mechanics should live in framework code, such as
|
|
`internal/framework/validators`, not in concrete validator packages. This package
|
|
can own shared helpers for decision cardinality, approval/rejection construction,
|
|
LLM validator batching, validator diagnostics, and Scriptorium request plumbing.
|
|
|
|
The concrete validator packages should own policy: what they inspect, what they
|
|
approve or reject, what warning reason codes they emit, and how they interpret
|
|
domain-specific data.
|
|
|
|
## Validator Design Policy
|
|
|
|
Validators should follow a small-tool model: each validator should do one thing
|
|
well. If a validator both rejects output and emits unrelated warnings, split
|
|
those concerns into separate validators so production mappings can include,
|
|
exclude, and order them independently.
|
|
|
|
Validators are read-only. A validator must not mutate pipeline state, rewrite
|
|
module output, materialize raw output into typed stage output, or enrich the
|
|
`ModuleOutput` passed to later validators. A validator returns an
|
|
accept/reject verdict for the output it evaluates, plus any warnings or
|
|
diagnostic references. Any conversion from raw module output into a downstream
|
|
representation is a separate materialization concern and must not be hidden
|
|
inside a validator.
|
|
|
|
The initial generic validator set should include:
|
|
|
|
- `generic/always_accept`: accepts returned module output unchanged. This is
|
|
functionally equivalent to a no-op validator and is primarily useful for tests,
|
|
demonstrations, and explicit pass-through configurations.
|
|
- `generic/always_reject`: rejects returned module output without inspecting it.
|
|
This is primarily useful for tests and for proving rejection plumbing,
|
|
manifests, and diagnostics.
|
|
- `generic/valid_json`: inspects raw returned module output and accepts only
|
|
syntactically valid JSON.
|
|
- `generic/valid_json_schema`: compares raw returned JSON with the module's
|
|
configured response schema and accepts only schema-conformant output.
|
|
|
|
The exact keys may be adjusted during implementation to match local naming
|
|
conventions, but the validator set should preserve these four behaviors.
|
|
|
|
For the current D&D spell behavior, the target split is:
|
|
|
|
- `generic/valid_json`: rejects returned module output that is not syntactically
|
|
valid JSON.
|
|
- `generic/valid_json_schema`: rejects returned JSON that does not conform to
|
|
the configured response schema.
|
|
- `extract/dnd/spells/shape`: rejects malformed spell-cast payloads and missing
|
|
required spell fields.
|
|
- `extract/dnd/spells/source_refs`: rejects missing or invalid source
|
|
references.
|
|
- `extract/dnd/spells/source_relatedness`: warning-only validator that reports
|
|
when a spell name is not found in the cited source text.
|
|
|
|
Production default mappings should generally list deterministic validators
|
|
before LLM-backed validators. This keeps cheap structural failures from consuming
|
|
model calls and keeps diagnostics easier to interpret. Pipeline-configured order
|
|
should still be authoritative; if a user explicitly lists an LLM-backed
|
|
validator before a deterministic validator, the framework should honor that
|
|
order rather than silently reshuffling it.
|
|
|
|
## Execution Classes
|
|
|
|
Validator specs should declare an execution class:
|
|
|
|
```go
|
|
type ExecutionClass string
|
|
|
|
const (
|
|
ExecutionClassDeterministic ExecutionClass = "deterministic"
|
|
ExecutionClassLLMBacked ExecutionClass = "llm_backed"
|
|
)
|
|
```
|
|
|
|
Execution class should be metadata on the validator spec or registered
|
|
definition, not an ad hoc convention inferred from package paths. It should be
|
|
used for:
|
|
|
|
- human-readable catalog and manifest reporting;
|
|
- diagnostics and timing summaries;
|
|
- operational policy such as concurrency budgeting for LLM-backed validators;
|
|
- default mapping review, where deterministic validators should usually appear
|
|
before LLM-backed validators.
|
|
|
|
Execution class should not by itself imply compatibility with a stage or module.
|
|
|
|
## Validator Contract
|
|
|
|
The validator framework should support validation of outputs from `chunk`,
|
|
`extract`, `merge`, and `normalize` stages. The contract should be generalized
|
|
enough for stage-specific validators to inspect the output they care about while
|
|
ignoring irrelevant fields.
|
|
|
|
The request should carry:
|
|
|
|
- stage name;
|
|
- module key;
|
|
- raw module output content when available;
|
|
- response schema metadata when the module declares one;
|
|
- source document;
|
|
- source input material;
|
|
- session ID;
|
|
- references;
|
|
- LLM client and profile for LLM-backed validators;
|
|
- options and metadata;
|
|
- chunk output when validating a chunk module;
|
|
- stage-specific typed envelopes when the stage owns them, such as chunk
|
|
envelopes for chunk validation.
|
|
|
|
A shared `ModuleOutput` envelope should represent the validation boundary.
|
|
Validators may inspect raw returned content and any already-existing typed
|
|
stage output, but they must not modify it.
|
|
|
|
Conceptually:
|
|
|
|
```go
|
|
type ModuleOutput struct {
|
|
Stage pipeline.Stage
|
|
ModuleKey string
|
|
|
|
RawContent []byte
|
|
MediaType string
|
|
ResponseSchema *llm.ResponseSchemaMetadata
|
|
|
|
Chunks []contracts.SourceChunk
|
|
Warnings []contracts.Warning
|
|
}
|
|
```
|
|
|
|
The final implementation does not need to use this exact shape, but it should
|
|
preserve the boundary: returned raw module output can enter validation before
|
|
any separate materialization step converts it into a stage-specific typed
|
|
representation.
|
|
|
|
The result should continue to express validator identity, warnings, and explicit
|
|
decisions. For output collections, the implementation should define an explicit
|
|
decision shape rather than silently mutating lists. Validator decisions reject or
|
|
approve output; validators do not rewrite output.
|
|
|
|
An empty validator list is always valid. With no validators, the framework should
|
|
pass module output forward unchanged and treat the output as approved for that
|
|
validation point. If the approved output cannot be consumed by a later stage
|
|
because its media type or envelope shape is unsuitable, that failure should be
|
|
reported at the downstream boundary that requires a different shape, not as an
|
|
implicit pre-validation rejection.
|
|
|
|
## Module Development Workflow
|
|
|
|
The validation system should make iterative module development easier. A module
|
|
author should be able to start with an explicit empty validator mapping and
|
|
inspect returned raw LLM output without first satisfying JSON syntax, schema,
|
|
media-type, or domain validators.
|
|
|
|
A typical development path should be:
|
|
|
|
1. Configure an empty validator set for the module and inspect raw returned
|
|
output.
|
|
2. Add `generic/valid_json` and adjust prompts until the model reliably returns
|
|
syntactically valid JSON.
|
|
3. Add `generic/valid_json_schema` and iterate on prompt/schema alignment.
|
|
4. Add media-type or schema validators appropriate to the module's intended
|
|
output format.
|
|
5. Add domain-specific validators one at a time until production policy is
|
|
represented explicitly in the chain.
|
|
|
|
This workflow is a central reason for making output validation explicit and
|
|
composable rather than hiding schema, shape, or domain checks inside module
|
|
implementation code.
|
|
|
|
## Central Production Mappings
|
|
|
|
Production defaults should be defined in a central, human-readable location near
|
|
the production module and validator registries. The mapping should be keyed by
|
|
stage and module key, not only by module key, so future modules can share keys
|
|
only when stage context makes their ownership unambiguous.
|
|
|
|
Conceptually:
|
|
|
|
```go
|
|
{
|
|
Stage: pipeline.StageExtract,
|
|
Module: "dnd/spells",
|
|
Validators: []pipeline.ModuleBinding{
|
|
pipeline.Binding("generic/valid_json"),
|
|
pipeline.Binding("generic/valid_json_schema"),
|
|
pipeline.Binding("extract/dnd/spells/shape"),
|
|
pipeline.Binding("extract/dnd/spells/source_refs"),
|
|
pipeline.Binding("extract/dnd/spells/source_relatedness"),
|
|
},
|
|
}
|
|
```
|
|
|
|
The production catalog should expose three related surfaces together:
|
|
|
|
- available modules;
|
|
- available validators;
|
|
- default module-to-validator mappings.
|
|
|
|
This makes production validation policy reviewable without constructing concrete
|
|
modules or searching inside module implementation packages.
|
|
|
|
The validator registry should expose registered validator specs without building
|
|
validators, including key and execution class. Building a validator should still
|
|
be available for runtime execution.
|
|
|
|
## Pipeline Overrides
|
|
|
|
Pipeline configuration should be able to override the central default mapping
|
|
for a module binding. Override semantics should distinguish three states:
|
|
|
|
- unset validators: use the central production/default mapping;
|
|
- explicit empty validators: run no validators and pass output forward;
|
|
- explicit non-empty validators: run exactly the configured validators in the
|
|
configured order.
|
|
|
|
This keeps the happy path concise while preserving advanced control for
|
|
experimentation, debugging, and custom deployments.
|
|
|
|
The run manifest should record the resolved validator chain for each validation
|
|
point so completed runs remain auditable after defaults or configuration change.
|
|
Each manifest entry should include at least validator key and execution class,
|
|
and should preserve the resolved order actually used for the run.
|
|
|
|
## LLM-Backed Validator Runtime
|
|
|
|
LLM-backed validators should use the same validator contract as deterministic
|
|
validators. Shared framework runtime should provide common support for:
|
|
|
|
- Scriptorium request construction;
|
|
- validator prompt and schema provenance;
|
|
- diagnostics redaction;
|
|
- optional batching or context-window controls when validator inputs are large;
|
|
- mapping successful LLM validator responses into validator decisions and
|
|
warnings;
|
|
- consistent handling of Scriptorium/runtime errors.
|
|
|
|
Scriptorium errors raised during validator execution should be treated as
|
|
validator execution errors unless a specific validator deliberately converts a
|
|
successful response into reject/warn decisions. This keeps provider/runtime
|
|
failure distinct from a validator's semantic rejection of module output.
|
|
|
|
## Stage Coverage
|
|
|
|
Validators should be composable across all LLM-eligible stages:
|
|
|
|
- `chunk`: validators can evaluate chunk boundaries, coverage, overlap, metadata,
|
|
or module-specific chunk quality.
|
|
- `extract`: validators can evaluate raw extracted output, source references,
|
|
payload shape, evidence quality, media type, or domain constraints.
|
|
- `merge`: validators can evaluate merged output, cross-chunk consistency,
|
|
deduplication results, media type, or domain-specific reconciliation.
|
|
- `normalize`: validators can evaluate normalized output, final shape,
|
|
post-processing results, media type, or domain-specific policy.
|
|
|
|
The framework should not require compatibility declarations in this pass. A
|
|
validator mapped to an unsuitable output shape should return a clear error, or
|
|
approve unchanged only when that is explicitly the validator's documented
|
|
behavior.
|
|
|
|
## Documentation Impact
|
|
|
|
When implemented, current-behavior docs and policy should be updated together:
|
|
|
|
- `docs/policy/architecture.md` should describe centralized validator mappings
|
|
rather than module-owned validator chains.
|
|
- `docs/internal/modules.md` should remove claims that concrete modules own
|
|
validator defaults.
|
|
- Internal validation docs should describe validator package ownership, mapping
|
|
precedence, empty-chain approval behavior, and LLM-backed validator support.
|
|
- User/config docs should describe how pipeline validator overrides work once the
|
|
syntax is implemented.
|
|
|
|
Roadmap docs should not remain the canonical description of implemented
|
|
validation behavior after the refactor is complete.
|