Final cleanup after checkpoint 3 and remove the completed implementation plan
This commit is contained in:
@@ -211,6 +211,13 @@ envelope with `json.RawMessage` payloads; extract modules should own typed Go
|
|||||||
structs at their boundaries and encode into that generic envelope before
|
structs at their boundaries and encode into that generic envelope before
|
||||||
returning to framework code.
|
returning to framework code.
|
||||||
|
|
||||||
|
Output-stage warnings are intentionally out-of-band. An `OutputEncoder` may
|
||||||
|
return warnings about serialization, formatting, truncation, or destination
|
||||||
|
concerns, but those warnings are appended to the runner result after encoding
|
||||||
|
and are not expected to appear inside the encoded artifact bytes. CLI,
|
||||||
|
diagnostic, or reporting layers should surface output-stage warnings from the
|
||||||
|
runner result.
|
||||||
|
|
||||||
Schemas should be versioned per extractor, with a separate envelope/manifest
|
Schemas should be versioned per extractor, with a separate envelope/manifest
|
||||||
format version.
|
format version.
|
||||||
|
|
||||||
|
|||||||
@@ -1,891 +0,0 @@
|
|||||||
# Implementation Plan: Checkpoint 3 Pipeline Stages
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
This is a staged implementation plan for
|
|
||||||
[`3-pipeline-stages-chunking-merge-normalize.md`](3-pipeline-stages-chunking-merge-normalize.md).
|
|
||||||
It is intended for an LLM coding agent to follow stage by stage.
|
|
||||||
|
|
||||||
This plan implements only checkpoint 3. Do not add real Seriatim parsing, real
|
|
||||||
D&D extraction, production config file loading, CLI command behavior, LLM
|
|
||||||
provider clients, prompt assets, response schema assets, diagnostics run
|
|
||||||
directories, or durable output writing.
|
|
||||||
|
|
||||||
## Policy Context
|
|
||||||
|
|
||||||
Follow:
|
|
||||||
|
|
||||||
- [`docs/policy/architecture.md`](../policy/architecture.md)
|
|
||||||
- [`docs/policy/documentation.md`](../policy/documentation.md)
|
|
||||||
|
|
||||||
Required boundaries:
|
|
||||||
|
|
||||||
- framework code must stay source-agnostic and domain-agnostic;
|
|
||||||
- concrete business logic remains under `internal/modules/<stage>/...`, but this
|
|
||||||
checkpoint should use fake modules only;
|
|
||||||
- framework plumbing should remain consolidated under `internal/framework/pipeline`
|
|
||||||
unless a boundary proves itself;
|
|
||||||
- capabilities are flat strings, not a type system;
|
|
||||||
- pipeline profiles are fixed-shape templates, not arbitrary step lists;
|
|
||||||
- no structural module wiring through ad hoc CLI flags.
|
|
||||||
|
|
||||||
## Global Implementation Decisions
|
|
||||||
|
|
||||||
- Add no third-party dependencies.
|
|
||||||
- Keep new checkpoint 3 framework code in `internal/framework/pipeline`.
|
|
||||||
- Keep shared request/result contracts in `internal/framework/contracts` when
|
|
||||||
stage modules will implement or consume them.
|
|
||||||
- Do not create new framework packages for chunking, merge, normalize, output,
|
|
||||||
warnings, response schemas, or structured output.
|
|
||||||
- Use in-memory pipeline profile structs only. Do not implement YAML/TOML
|
|
||||||
parsing in this checkpoint.
|
|
||||||
- Keep one shared chunker per resolved pipeline. Per-lane chunker overrides are
|
|
||||||
deferred.
|
|
||||||
- Execute chunks serially in checkpoint 3. Contracts must not prevent later
|
|
||||||
parallel execution.
|
|
||||||
- Sort artifact lane IDs during resolution for deterministic output and digest
|
|
||||||
behavior.
|
|
||||||
- Use `sha256:<hex>` for resolved pipeline digests.
|
|
||||||
- Preserve existing validator decision-cardinality behavior.
|
|
||||||
- Run `gofmt` on all touched Go files before validation.
|
|
||||||
|
|
||||||
## Stage 1: Stage Contracts And Source Chunks
|
|
||||||
|
|
||||||
### Goal
|
|
||||||
|
|
||||||
Define the source chunk model and contracts for chunk, merge, normalize, and
|
|
||||||
output stages.
|
|
||||||
|
|
||||||
### Files To Update
|
|
||||||
|
|
||||||
- `internal/framework/contracts/contracts.go`
|
|
||||||
- `internal/framework/contracts/contracts_test.go`
|
|
||||||
- `internal/framework/contracts/composition_test.go`
|
|
||||||
|
|
||||||
### Required API
|
|
||||||
|
|
||||||
Add:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type SourceChunk struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
SourceID string `json:"source_id"`
|
|
||||||
Index int `json:"index"`
|
|
||||||
Units []source.SourceUnit `json:"units"`
|
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChunkRequest struct {
|
|
||||||
Source *source.SourceDocument `json:"-"`
|
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChunkResult struct {
|
|
||||||
Chunks []SourceChunk `json:"chunks"`
|
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Chunker interface {
|
|
||||||
Key() string
|
|
||||||
Chunk(ctx context.Context, req ChunkRequest) (ChunkResult, error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Extend `ExtractionRequest`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ExtractionRequest struct {
|
|
||||||
Source *source.SourceDocument `json:"-"`
|
|
||||||
Chunk *SourceChunk `json:"chunk,omitempty"`
|
|
||||||
AmbientContext map[string]any `json:"ambient_context,omitempty"`
|
|
||||||
LLMClient StructuredLLMClient `json:"-"`
|
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ChunkArtifacts struct {
|
|
||||||
Chunk SourceChunk `json:"chunk"`
|
|
||||||
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MergeRequest struct {
|
|
||||||
Source *source.SourceDocument `json:"-"`
|
|
||||||
LaneID string `json:"lane_id"`
|
|
||||||
ChunkArtifacts []ChunkArtifacts `json:"chunk_artifacts"`
|
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MergeResult struct {
|
|
||||||
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Merger interface {
|
|
||||||
Key() string
|
|
||||||
Merge(ctx context.Context, req MergeRequest) (MergeResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type NormalizeRequest struct {
|
|
||||||
Source *source.SourceDocument `json:"-"`
|
|
||||||
LaneID string `json:"lane_id"`
|
|
||||||
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NormalizeResult struct {
|
|
||||||
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Normalizer interface {
|
|
||||||
Key() string
|
|
||||||
Normalize(ctx context.Context, req NormalizeRequest) (NormalizeResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputRequest struct {
|
|
||||||
Manifest artifacts.RunManifest `json:"manifest"`
|
|
||||||
Approved []artifacts.Artifact `json:"approved,omitempty"`
|
|
||||||
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
|
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputResult struct {
|
|
||||||
Bytes []byte `json:"-"`
|
|
||||||
ContentType string `json:"content_type,omitempty"`
|
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputEncoder interface {
|
|
||||||
Key() string
|
|
||||||
Encode(ctx context.Context, req OutputRequest) (OutputResult, error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If `gofmt` aligns fields differently, keep the formatted output.
|
|
||||||
|
|
||||||
### Required Behavior
|
|
||||||
|
|
||||||
- Existing fake extractor tests must still compile with `ExtractionRequest{Source: doc}`.
|
|
||||||
- `SourceChunk` is a framework contract type, not a core source type.
|
|
||||||
- `SourceChunk.Units` should be copied by implementations where mutation risk
|
|
||||||
exists; this stage only defines the contract.
|
|
||||||
|
|
||||||
### Required Tests
|
|
||||||
|
|
||||||
Update fake implementations in existing contract tests to prove:
|
|
||||||
|
|
||||||
- fake chunker satisfies `contracts.Chunker`;
|
|
||||||
- fake merger satisfies `contracts.Merger`;
|
|
||||||
- fake normalizer satisfies `contracts.Normalizer`;
|
|
||||||
- fake output encoder satisfies `contracts.OutputEncoder`;
|
|
||||||
- fake extractor can receive a `SourceChunk` and ambient context.
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -w internal/framework/contracts
|
|
||||||
go test ./internal/framework/contracts
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stage 2: Module Metadata And Stage Registries
|
|
||||||
|
|
||||||
### Goal
|
|
||||||
|
|
||||||
Teach pipeline registries about module metadata and add registries for chunk,
|
|
||||||
merge, normalize, configured validators, and output modules.
|
|
||||||
|
|
||||||
### Files To Update
|
|
||||||
|
|
||||||
- `internal/framework/pipeline/input_registry.go`
|
|
||||||
- `internal/framework/pipeline/input_registry_test.go`
|
|
||||||
- `internal/framework/pipeline/extractor_registry.go`
|
|
||||||
- `internal/framework/pipeline/extractor_registry_test.go`
|
|
||||||
|
|
||||||
### Files To Add
|
|
||||||
|
|
||||||
- `internal/framework/pipeline/module.go`
|
|
||||||
- `internal/framework/pipeline/chunker_registry.go`
|
|
||||||
- `internal/framework/pipeline/chunker_registry_test.go`
|
|
||||||
- `internal/framework/pipeline/merger_registry.go`
|
|
||||||
- `internal/framework/pipeline/merger_registry_test.go`
|
|
||||||
- `internal/framework/pipeline/normalizer_registry.go`
|
|
||||||
- `internal/framework/pipeline/normalizer_registry_test.go`
|
|
||||||
- `internal/framework/pipeline/validator_registry.go`
|
|
||||||
- `internal/framework/pipeline/validator_registry_test.go`
|
|
||||||
- `internal/framework/pipeline/output_registry.go`
|
|
||||||
- `internal/framework/pipeline/output_registry_test.go`
|
|
||||||
|
|
||||||
### Required API
|
|
||||||
|
|
||||||
Add:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ModuleStage string
|
|
||||||
|
|
||||||
const (
|
|
||||||
StageInput ModuleStage = "input"
|
|
||||||
StageChunk ModuleStage = "chunk"
|
|
||||||
StageExtract ModuleStage = "extract"
|
|
||||||
StageMerge ModuleStage = "merge"
|
|
||||||
StageNormalize ModuleStage = "normalize"
|
|
||||||
StageValidate ModuleStage = "validate"
|
|
||||||
StageOutput ModuleStage = "output"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ModuleSpec struct {
|
|
||||||
Key string
|
|
||||||
Stage ModuleStage
|
|
||||||
Provides []string
|
|
||||||
Requires []string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add metadata registration methods to existing registries:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (r *InputAdapterRegistry) RegisterWithSpec(spec ModuleSpec, constructor InputAdapterConstructor) error
|
|
||||||
func (r *InputAdapterRegistry) Spec(key string) (ModuleSpec, bool)
|
|
||||||
|
|
||||||
func (r *ExtractorRegistry) RegisterWithSpec(spec ModuleSpec, constructor ExtractorConstructor) error
|
|
||||||
func (r *ExtractorRegistry) Spec(key string) (ModuleSpec, bool)
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep existing `Register` methods. They should call `RegisterWithSpec` with a
|
|
||||||
default spec using the provided key and correct stage.
|
|
||||||
|
|
||||||
Add typed registries for other stages:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ChunkerConstructor func() (contracts.Chunker, error)
|
|
||||||
type ChunkerRegistry struct { /* unexported fields */ }
|
|
||||||
func NewChunkerRegistry() *ChunkerRegistry
|
|
||||||
func (r *ChunkerRegistry) Register(key string, constructor ChunkerConstructor) error
|
|
||||||
func (r *ChunkerRegistry) RegisterWithSpec(spec ModuleSpec, constructor ChunkerConstructor) error
|
|
||||||
func (r *ChunkerRegistry) Build(key string) (contracts.Chunker, error)
|
|
||||||
func (r *ChunkerRegistry) Spec(key string) (ModuleSpec, bool)
|
|
||||||
func (r *ChunkerRegistry) RegisteredKeys() []string
|
|
||||||
```
|
|
||||||
|
|
||||||
Repeat the same pattern for `MergerRegistry`, `NormalizerRegistry`,
|
|
||||||
`ValidatorRegistry`, and `OutputEncoderRegistry`.
|
|
||||||
|
|
||||||
`ValidatorRegistry` should use:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ValidatorConstructor func() (contracts.Validator, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
Because `contracts.Validator` uses `Name()` rather than `Key()`,
|
|
||||||
`ValidatorRegistry.Build` must require `validator.Name()` to match the
|
|
||||||
registered key.
|
|
||||||
|
|
||||||
### Required Behavior
|
|
||||||
|
|
||||||
- Registry keys are trimmed.
|
|
||||||
- Specs are normalized by trimming `Key`, `Provides`, and `Requires`.
|
|
||||||
- Empty capability strings are ignored.
|
|
||||||
- `Provides` and `Requires` are deduplicated and sorted for deterministic
|
|
||||||
behavior.
|
|
||||||
- `RegisterWithSpec` rejects an empty key, wrong stage, nil constructor, and
|
|
||||||
duplicate key.
|
|
||||||
- `Spec` returns false for unknown keys and nil registries.
|
|
||||||
- `Build` rejects unknown keys, constructor errors, nil modules, and key
|
|
||||||
mismatches.
|
|
||||||
- Existing registry tests continue to pass.
|
|
||||||
|
|
||||||
### Required Tests
|
|
||||||
|
|
||||||
For each new registry, cover the same behavior as existing input/extractor
|
|
||||||
registries:
|
|
||||||
|
|
||||||
- successful registration and build;
|
|
||||||
- metadata registration and lookup;
|
|
||||||
- capability normalization;
|
|
||||||
- wrong stage rejection;
|
|
||||||
- key trimming;
|
|
||||||
- empty key rejection;
|
|
||||||
- duplicate key rejection;
|
|
||||||
- nil constructor rejection;
|
|
||||||
- unknown key build error;
|
|
||||||
- constructor error wrapping;
|
|
||||||
- nil module rejection;
|
|
||||||
- key mismatch rejection;
|
|
||||||
- sorted `RegisteredKeys`;
|
|
||||||
- nil registry behavior.
|
|
||||||
|
|
||||||
For `ValidatorRegistry`, key mismatch checks should compare the registry key to
|
|
||||||
`Validator.Name()`.
|
|
||||||
|
|
||||||
For existing input/extractor registries, add tests for metadata registration,
|
|
||||||
default specs from `Register`, capability normalization, and wrong stage
|
|
||||||
rejection.
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -w internal/framework/pipeline
|
|
||||||
go test ./internal/framework/pipeline
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stage 3: Resolved Pipeline Model
|
|
||||||
|
|
||||||
### Goal
|
|
||||||
|
|
||||||
Add fixed-shape pipeline profile structs, default resolution, lane selection,
|
|
||||||
capability validation, and resolved pipeline digesting.
|
|
||||||
|
|
||||||
### Files To Add
|
|
||||||
|
|
||||||
- `internal/framework/pipeline/profile.go`
|
|
||||||
- `internal/framework/pipeline/profile_test.go`
|
|
||||||
|
|
||||||
### Files To Update
|
|
||||||
|
|
||||||
- `internal/core/artifacts/artifacts.go`
|
|
||||||
- `internal/core/artifacts/artifacts_test.go`
|
|
||||||
|
|
||||||
### Required API
|
|
||||||
|
|
||||||
Update `artifacts.RunManifest` to include:
|
|
||||||
|
|
||||||
```go
|
|
||||||
PipelineID string `json:"pipeline_id,omitempty"`
|
|
||||||
PipelineDigest string `json:"pipeline_digest,omitempty"`
|
|
||||||
```
|
|
||||||
|
|
||||||
Add lane-level manifest details rather than relying on the existing shared
|
|
||||||
`Extractors`, `Merger`, or `Normalizer` fields for multi-lane runs:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ArtifactLaneManifest struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Extractor string `json:"extractor"`
|
|
||||||
Merger string `json:"merger"`
|
|
||||||
Normalizer string `json:"normalizer"`
|
|
||||||
Validators []string `json:"validators,omitempty"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then add:
|
|
||||||
|
|
||||||
```go
|
|
||||||
ArtifactLanes []ArtifactLaneManifest `json:"artifact_lanes,omitempty"`
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not remove the existing singular fields in this checkpoint unless doing so is
|
|
||||||
required to make the package compile. Treat `ArtifactLanes` as the authoritative
|
|
||||||
manifest representation for checkpoint 3's multi-lane pipeline model.
|
|
||||||
|
|
||||||
Add pipeline model types:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ModuleBinding struct {
|
|
||||||
Module string `json:"module"`
|
|
||||||
LLMProfile string `json:"llm_profile,omitempty"`
|
|
||||||
Options map[string]any `json:"options,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArtifactLaneProfile struct {
|
|
||||||
Extract ModuleBinding `json:"extract"`
|
|
||||||
Merge ModuleBinding `json:"merge,omitempty"`
|
|
||||||
Normalize ModuleBinding `json:"normalize,omitempty"`
|
|
||||||
Validators []ModuleBinding `json:"validators,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PipelineProfile struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Input ModuleBinding `json:"input"`
|
|
||||||
Chunk ModuleBinding `json:"chunk,omitempty"`
|
|
||||||
Artifacts map[string]ArtifactLaneProfile `json:"artifacts"`
|
|
||||||
Output ModuleBinding `json:"output,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResolveOptions struct {
|
|
||||||
Only []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResolvedArtifactLane struct {
|
|
||||||
ID string
|
|
||||||
Extract ModuleBinding
|
|
||||||
Merge ModuleBinding
|
|
||||||
Normalize ModuleBinding
|
|
||||||
Validators []ModuleBinding
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResolvedPipeline struct {
|
|
||||||
ID string
|
|
||||||
Digest string
|
|
||||||
Input ModuleBinding
|
|
||||||
Chunk ModuleBinding
|
|
||||||
ArtifactLanes []ResolvedArtifactLane
|
|
||||||
Output ModuleBinding
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add defaults:
|
|
||||||
|
|
||||||
```go
|
|
||||||
const (
|
|
||||||
DefaultChunkModule = "generic"
|
|
||||||
DefaultMergeModule = "appendorder"
|
|
||||||
DefaultNormalizeModule = "noop"
|
|
||||||
DefaultOutputModule = "json"
|
|
||||||
DefaultLLMProfile = "default"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Add:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ModuleCatalog struct {
|
|
||||||
Inputs *InputAdapterRegistry
|
|
||||||
Chunkers *ChunkerRegistry
|
|
||||||
Extractors *ExtractorRegistry
|
|
||||||
Mergers *MergerRegistry
|
|
||||||
Normalizers *NormalizerRegistry
|
|
||||||
Validators *ValidatorRegistry
|
|
||||||
Outputs *OutputEncoderRegistry
|
|
||||||
}
|
|
||||||
|
|
||||||
func Binding(module string) ModuleBinding
|
|
||||||
func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog ModuleCatalog) (ResolvedPipeline, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Required Behavior
|
|
||||||
|
|
||||||
`ResolvePipeline` must:
|
|
||||||
|
|
||||||
- trim `PipelineProfile.ID`;
|
|
||||||
- reject empty pipeline IDs;
|
|
||||||
- require non-empty input module;
|
|
||||||
- require at least one artifact lane before and after `Only` filtering;
|
|
||||||
- apply defaults for missing chunk, merge, normalize, output, and binding
|
|
||||||
`llm_profile` values;
|
|
||||||
- trim all module keys;
|
|
||||||
- trim lane names in `ResolveOptions.Only`;
|
|
||||||
- reject empty or unknown lane names in `ResolveOptions.Only`;
|
|
||||||
- deduplicate repeated `Only` lane names;
|
|
||||||
- sort selected lane IDs alphabetically;
|
|
||||||
- reject unknown module keys using registry metadata, not constructors;
|
|
||||||
- validate flat capability requirements before execution;
|
|
||||||
- compute `ResolvedPipeline.Digest` from canonical JSON of the resolved
|
|
||||||
pipeline excluding the digest field itself.
|
|
||||||
|
|
||||||
Capability validation algorithm:
|
|
||||||
|
|
||||||
1. Start with capabilities provided by the input module.
|
|
||||||
2. Validate chunker requirements against current capabilities, then add chunker
|
|
||||||
provided capabilities.
|
|
||||||
3. For each selected lane, start with input plus chunk capabilities.
|
|
||||||
4. Validate extractor requirements, then add extractor provided capabilities.
|
|
||||||
5. Validate merger requirements, then add merger provided capabilities.
|
|
||||||
6. Validate normalizer requirements, then add normalizer provided capabilities.
|
|
||||||
7. Validate configured validator requirements in lane order, then add validator
|
|
||||||
provided capabilities. If no validators are configured for a lane, do not
|
|
||||||
attempt capability validation for extractor-owned default validators.
|
|
||||||
8. Validate output requirements against the union of pipeline-level capabilities
|
|
||||||
and all selected lane capabilities.
|
|
||||||
|
|
||||||
Error messages must name the pipeline ID, lane ID when applicable, stage, module
|
|
||||||
key, and missing capability when applicable.
|
|
||||||
|
|
||||||
`Binding(module)` should trim the module key and leave `LLMProfile` empty and
|
|
||||||
`Options` nil before resolution. Resolution should fill empty `LLMProfile`
|
|
||||||
values with `DefaultLLMProfile` on all resolved bindings.
|
|
||||||
|
|
||||||
### Required Tests
|
|
||||||
|
|
||||||
Cover:
|
|
||||||
|
|
||||||
- successful resolution with explicit modules;
|
|
||||||
- default chunk, merge, normalize, output, and `llm_profile`;
|
|
||||||
- lane selection with `Only`;
|
|
||||||
- `Only` trimming and deduplication;
|
|
||||||
- unknown `Only` lane rejection;
|
|
||||||
- empty `Only` lane rejection;
|
|
||||||
- empty artifact set rejection;
|
|
||||||
- empty pipeline ID rejection;
|
|
||||||
- missing input rejection;
|
|
||||||
- unknown module key rejection for each stage;
|
|
||||||
- missing capability rejection for input/chunk/extract/merge/normalize/validate/output
|
|
||||||
interactions;
|
|
||||||
- deterministic lane ordering;
|
|
||||||
- deterministic digest for equivalent input maps;
|
|
||||||
- digest changes when a module binding changes;
|
|
||||||
- manifest JSON still omits empty optional fields.
|
|
||||||
|
|
||||||
Use fake registries and fake specs only. Do not add config file parsing.
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -w internal/core/artifacts internal/framework/pipeline
|
|
||||||
go test ./internal/core/artifacts
|
|
||||||
go test ./internal/framework/pipeline
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stage 4: Generic Merge And Normalize Helpers
|
|
||||||
|
|
||||||
### Goal
|
|
||||||
|
|
||||||
Add generic append-in-chunk-order merge and no-op normalization behavior inside
|
|
||||||
`internal/framework/pipeline`.
|
|
||||||
|
|
||||||
### Files To Add
|
|
||||||
|
|
||||||
- `internal/framework/pipeline/generic_stages.go`
|
|
||||||
- `internal/framework/pipeline/generic_stages_test.go`
|
|
||||||
|
|
||||||
### Required API
|
|
||||||
|
|
||||||
Add:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type AppendOrderMerger struct{}
|
|
||||||
func (m AppendOrderMerger) Key() string
|
|
||||||
func (m AppendOrderMerger) Merge(ctx context.Context, req contracts.MergeRequest) (contracts.MergeResult, error)
|
|
||||||
|
|
||||||
type NoopNormalizer struct{}
|
|
||||||
func (n NoopNormalizer) Key() string
|
|
||||||
func (n NoopNormalizer) Normalize(ctx context.Context, req contracts.NormalizeRequest) (contracts.NormalizeResult, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Required Behavior
|
|
||||||
|
|
||||||
`AppendOrderMerger` must:
|
|
||||||
|
|
||||||
- return key `appendorder`;
|
|
||||||
- concatenate candidates by `ChunkArtifacts` slice order and candidate order;
|
|
||||||
- copy candidate slices so caller mutation does not affect the result;
|
|
||||||
- preserve source refs and metadata exactly;
|
|
||||||
- append no warnings.
|
|
||||||
|
|
||||||
`NoopNormalizer` must:
|
|
||||||
|
|
||||||
- return key `noop`;
|
|
||||||
- copy candidates so caller mutation does not affect the result;
|
|
||||||
- preserve ordering and source refs exactly;
|
|
||||||
- append no warnings.
|
|
||||||
|
|
||||||
### Required Tests
|
|
||||||
|
|
||||||
Cover:
|
|
||||||
|
|
||||||
- key values;
|
|
||||||
- append order across multiple chunks;
|
|
||||||
- stable candidate order within chunks;
|
|
||||||
- no-op normalize preserves order;
|
|
||||||
- result slices are caller-mutation safe;
|
|
||||||
- nil/empty input returns empty candidates and no error.
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -w internal/framework/pipeline
|
|
||||||
go test ./internal/framework/pipeline
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stage 5: Pipeline Runner Refactor
|
|
||||||
|
|
||||||
### Goal
|
|
||||||
|
|
||||||
Refactor the existing runner from extractor-only execution to resolved pipeline
|
|
||||||
execution across input, chunk, extract, merge, normalize, validate, and output.
|
|
||||||
|
|
||||||
### Files To Update
|
|
||||||
|
|
||||||
- `internal/framework/pipeline/runner.go`
|
|
||||||
- `internal/framework/pipeline/runner_test.go`
|
|
||||||
- `internal/framework/pipeline/registry_integration_test.go`
|
|
||||||
|
|
||||||
### Required API
|
|
||||||
|
|
||||||
Replace the extractor-only runner constructor with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Registries struct {
|
|
||||||
Inputs *InputAdapterRegistry
|
|
||||||
Chunkers *ChunkerRegistry
|
|
||||||
Extractors *ExtractorRegistry
|
|
||||||
Mergers *MergerRegistry
|
|
||||||
Normalizers *NormalizerRegistry
|
|
||||||
Validators *ValidatorRegistry
|
|
||||||
Outputs *OutputEncoderRegistry
|
|
||||||
}
|
|
||||||
|
|
||||||
type Runner struct {
|
|
||||||
// unexported fields
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(registries Registries) *Runner
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace `RunInput` with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type RunInput struct {
|
|
||||||
Pipeline ResolvedPipeline
|
|
||||||
SourceID string
|
|
||||||
Path string
|
|
||||||
RawInput []byte
|
|
||||||
LLMClient contracts.StructuredLLMClient
|
|
||||||
Metadata map[string]any
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace or extend `RunOutput` with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type RunOutput struct {
|
|
||||||
Manifest artifacts.RunManifest `json:"manifest"`
|
|
||||||
Approved []artifacts.Artifact `json:"approved,omitempty"`
|
|
||||||
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
|
|
||||||
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
||||||
EncodedOutput []byte `json:"-"`
|
|
||||||
ContentType string `json:"content_type,omitempty"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep the existing candidate metadata normalization behavior, but it should now
|
|
||||||
run per extractor and across all selected lanes.
|
|
||||||
|
|
||||||
### Required Behavior
|
|
||||||
|
|
||||||
`Run` must:
|
|
||||||
|
|
||||||
1. reject nil runner;
|
|
||||||
2. reject missing registries for stages used by the pipeline;
|
|
||||||
`Validators` is required only when at least one selected lane declares
|
|
||||||
validator bindings;
|
|
||||||
3. reject empty resolved pipeline ID or digest;
|
|
||||||
4. build the input adapter and parse `RawInput`;
|
|
||||||
5. validate the resulting `SourceDocument`;
|
|
||||||
6. build the chunker and produce chunks;
|
|
||||||
7. reject empty chunk results;
|
|
||||||
8. for each selected artifact lane in resolved order:
|
|
||||||
- build extractor, merger, and normalizer;
|
|
||||||
- extract from every chunk serially;
|
|
||||||
- pass `Source`, active `Chunk`, `LLMClient`, and metadata to the extractor;
|
|
||||||
- normalize candidate metadata with global monotonically increasing indices;
|
|
||||||
- merge per-chunk candidates;
|
|
||||||
- normalize merged candidates;
|
|
||||||
- run configured lane validators against normalized candidates when the lane
|
|
||||||
declares validators;
|
|
||||||
- otherwise run the extractor-owned default validator chain;
|
|
||||||
- append approved artifacts and rejected artifacts to output;
|
|
||||||
9. build the output encoder and encode the final output bundle;
|
|
||||||
10. populate `RunManifest` with pipeline ID, pipeline digest, input module,
|
|
||||||
chunker, lane-level extractor/merger/normalizer/validator keys, output
|
|
||||||
encoder, source digest, and validation status;
|
|
||||||
11. return partial output plus wrapped errors on failure.
|
|
||||||
|
|
||||||
Validation status:
|
|
||||||
|
|
||||||
- `approved` when all candidates from all lanes survive validation and no lane
|
|
||||||
returns rejected artifacts;
|
|
||||||
- `rejected` when at least one candidate is rejected and execution otherwise
|
|
||||||
succeeds;
|
|
||||||
- `failed` when returning an error after a manifest has been initialized.
|
|
||||||
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- collect warnings from input, chunk, extract, merge, normalize, validator, and
|
|
||||||
output stages where those stages expose warnings;
|
|
||||||
- existing input adapters do not expose warnings, so no input warnings are
|
|
||||||
required in this checkpoint.
|
|
||||||
|
|
||||||
Do not validate source references inside the runner in checkpoint 3. Source
|
|
||||||
reference validation remains a later deterministic validator.
|
|
||||||
|
|
||||||
### Required Tests
|
|
||||||
|
|
||||||
Refactor existing runner tests and add coverage for:
|
|
||||||
|
|
||||||
- nil runner error;
|
|
||||||
- missing registry errors;
|
|
||||||
- invalid resolved pipeline error;
|
|
||||||
- input adapter build and parse errors;
|
|
||||||
- invalid source document error;
|
|
||||||
- chunker build and chunk errors;
|
|
||||||
- empty chunk result error;
|
|
||||||
- extractor runs once per chunk;
|
|
||||||
- extractor receives active chunk and LLM client;
|
|
||||||
- merge receives per-chunk candidates;
|
|
||||||
- normalize receives merged candidates;
|
|
||||||
- validator approvals and rejections still work;
|
|
||||||
- configured validators are built and run in lane order;
|
|
||||||
- extractor-owned default validators run when a lane does not configure
|
|
||||||
validators;
|
|
||||||
- configured validators replace extractor-owned default validators when present;
|
|
||||||
- global candidate indices across lanes and chunks;
|
|
||||||
- warnings from chunk, extract, merge, normalize, validator, and output stages;
|
|
||||||
- output encoder receives manifest and artifacts;
|
|
||||||
- manifest pipeline ID, digest, and artifact lane details are populated;
|
|
||||||
- partial output is returned when a later lane fails.
|
|
||||||
|
|
||||||
Use fake modules only.
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -w internal/framework/pipeline
|
|
||||||
go test ./internal/framework/pipeline
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stage 6: Fixture-Driven Walking Skeleton
|
|
||||||
|
|
||||||
### Goal
|
|
||||||
|
|
||||||
Add an end-to-end fixture-driven integration test that exercises the full staged
|
|
||||||
path with fake modules and a fake LLM client.
|
|
||||||
|
|
||||||
### Files To Add
|
|
||||||
|
|
||||||
- `internal/framework/pipeline/testdata/walking_skeleton_input.json`
|
|
||||||
- `internal/framework/pipeline/testdata/walking_skeleton_output.json`
|
|
||||||
- `internal/framework/pipeline/walking_skeleton_test.go`
|
|
||||||
|
|
||||||
### Required Fixture Shape
|
|
||||||
|
|
||||||
Use a small input fixture with two or three source units in a simple fake JSON
|
|
||||||
format. Do not use Seriatim.
|
|
||||||
|
|
||||||
Example input shape:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "fixture-source",
|
|
||||||
"units": [
|
|
||||||
{"id": "u1", "text": "First event."},
|
|
||||||
{"id": "u2", "text": "Second event."},
|
|
||||||
{"id": "u3", "text": "Third event."}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The expected output fixture should contain:
|
|
||||||
|
|
||||||
- manifest with `pipeline_id` and `pipeline_digest`;
|
|
||||||
- approved artifacts emitted from multiple chunks;
|
|
||||||
- no rejected artifacts for the happy path;
|
|
||||||
- warnings only if fake stages deliberately emit them.
|
|
||||||
|
|
||||||
Compare expected output by unmarshaled structural equality, not raw bytes, so
|
|
||||||
formatting differences do not fail the test.
|
|
||||||
|
|
||||||
### Required Test Setup
|
|
||||||
|
|
||||||
Register fake modules with metadata:
|
|
||||||
|
|
||||||
- fake input adapter key `fake/input`, provides `plain_text`;
|
|
||||||
- fake chunker key `fake/chunk`, requires `plain_text`, provides `chunks`;
|
|
||||||
- fake extractor key `fake/extract`, requires `chunks`, provides
|
|
||||||
`fake_artifacts`;
|
|
||||||
- append-order merger key `appendorder`, requires `fake_artifacts`;
|
|
||||||
- no-op normalizer key `noop`;
|
|
||||||
- fake output encoder key `json`.
|
|
||||||
|
|
||||||
Use an in-memory `PipelineProfile`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
PipelineProfile{
|
|
||||||
ID: "walking-skeleton",
|
|
||||||
Input: Binding("fake/input"),
|
|
||||||
Chunk: Binding("fake/chunk"),
|
|
||||||
Artifacts: map[string]ArtifactLaneProfile{
|
|
||||||
"events": {Extract: Binding("fake/extract")},
|
|
||||||
},
|
|
||||||
Output: Binding("json"),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Resolve the profile, run the runner with fixture bytes and fake LLM client, then
|
|
||||||
assert the encoded output matches the expected fixture structurally.
|
|
||||||
|
|
||||||
### Required Behavior
|
|
||||||
|
|
||||||
- The fake input adapter parses raw fixture bytes into `SourceDocument`.
|
|
||||||
- The fake chunker produces at least two chunks.
|
|
||||||
- The fake extractor calls the fake structured LLM client once per chunk.
|
|
||||||
- The fake extractor returns one artifact candidate per chunk.
|
|
||||||
- The append-order merger preserves chunk order.
|
|
||||||
- The no-op normalizer preserves candidates.
|
|
||||||
- The fake output encoder produces deterministic JSON bytes.
|
|
||||||
- The test fails before execution if required capabilities are missing.
|
|
||||||
|
|
||||||
### Additional Tests
|
|
||||||
|
|
||||||
Add focused tests for the walking skeleton failure path:
|
|
||||||
|
|
||||||
- missing required capability fails during resolution;
|
|
||||||
- `Only` with an unknown lane fails during resolution;
|
|
||||||
- fake LLM call count equals chunk count in the happy path.
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -w internal/framework/pipeline
|
|
||||||
go test ./internal/framework/pipeline
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stage 7: Final Checkpoint 3 Review Pass
|
|
||||||
|
|
||||||
### Goal
|
|
||||||
|
|
||||||
Clean up naming, formatting, and accidental scope creep before checkpoint 3 is
|
|
||||||
considered complete.
|
|
||||||
|
|
||||||
### Required Review
|
|
||||||
|
|
||||||
Check:
|
|
||||||
|
|
||||||
- no Seriatim parser exists;
|
|
||||||
- no D&D module exists;
|
|
||||||
- no production config file loader exists;
|
|
||||||
- no CLI behavior was added;
|
|
||||||
- no LLM provider code exists;
|
|
||||||
- no prompt, response schema, diagnostics, or durable output package exists;
|
|
||||||
- no new framework package was created beyond `contracts`, `pipeline`, and
|
|
||||||
`validate`;
|
|
||||||
- stage contracts remain source/domain agnostic;
|
|
||||||
- capabilities remain flat strings;
|
|
||||||
- resolved pipeline profiles remain fixed-shape templates;
|
|
||||||
- walking skeleton uses fake modules only.
|
|
||||||
|
|
||||||
### Required Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -w internal/core internal/framework
|
|
||||||
go test ./...
|
|
||||||
go build ./cmd/notarius
|
|
||||||
```
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
None. The deferred choices identified while drafting this plan are intentionally
|
|
||||||
out of scope for checkpoint 3 and should remain deferred until later roadmap
|
|
||||||
items.
|
|
||||||
@@ -35,10 +35,12 @@ type StructuredLLMClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ParseRequest struct {
|
type ParseRequest struct {
|
||||||
SourceID string `json:"source_id,omitempty"`
|
SourceID string `json:"source_id,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
Raw []byte `json:"-"`
|
Raw []byte `json:"-"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputAdapter interface {
|
type InputAdapter interface {
|
||||||
@@ -55,8 +57,10 @@ type SourceChunk struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChunkRequest struct {
|
type ChunkRequest struct {
|
||||||
Source *source.SourceDocument `json:"-"`
|
Source *source.SourceDocument `json:"-"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChunkResult struct {
|
type ChunkResult struct {
|
||||||
@@ -74,6 +78,8 @@ type ExtractionRequest struct {
|
|||||||
Chunk *SourceChunk `json:"chunk,omitempty"`
|
Chunk *SourceChunk `json:"chunk,omitempty"`
|
||||||
AmbientContext map[string]any `json:"ambient_context,omitempty"`
|
AmbientContext map[string]any `json:"ambient_context,omitempty"`
|
||||||
LLMClient StructuredLLMClient `json:"-"`
|
LLMClient StructuredLLMClient `json:"-"`
|
||||||
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +105,8 @@ type MergeRequest struct {
|
|||||||
Source *source.SourceDocument `json:"-"`
|
Source *source.SourceDocument `json:"-"`
|
||||||
LaneID string `json:"lane_id"`
|
LaneID string `json:"lane_id"`
|
||||||
ChunkArtifacts []ChunkArtifacts `json:"chunk_artifacts"`
|
ChunkArtifacts []ChunkArtifacts `json:"chunk_artifacts"`
|
||||||
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +124,8 @@ type NormalizeRequest struct {
|
|||||||
Source *source.SourceDocument `json:"-"`
|
Source *source.SourceDocument `json:"-"`
|
||||||
LaneID string `json:"lane_id"`
|
LaneID string `json:"lane_id"`
|
||||||
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
||||||
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +142,8 @@ type Normalizer interface {
|
|||||||
type ValidationRequest struct {
|
type ValidationRequest struct {
|
||||||
Source *source.SourceDocument `json:"-"`
|
Source *source.SourceDocument `json:"-"`
|
||||||
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
Candidates []artifacts.ArtifactCandidate `json:"candidates"`
|
||||||
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,11 +173,13 @@ type Warning struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OutputRequest struct {
|
type OutputRequest struct {
|
||||||
Manifest artifacts.RunManifest `json:"manifest"`
|
Manifest artifacts.RunManifest `json:"manifest"`
|
||||||
Approved []artifacts.Artifact `json:"approved,omitempty"`
|
Approved []artifacts.Artifact `json:"approved,omitempty"`
|
||||||
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
|
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
|
||||||
Warnings []Warning `json:"warnings,omitempty"`
|
Warnings []Warning `json:"warnings,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
LLMProfile string `json:"llm_profile,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutputResult struct {
|
type OutputResult struct {
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
|||||||
return failOutput(output), fmt.Errorf("build input adapter %q: %w", input.Pipeline.Input.Module, err)
|
return failOutput(output), fmt.Errorf("build input adapter %q: %w", input.Pipeline.Input.Module, err)
|
||||||
}
|
}
|
||||||
doc, err := adapter.Parse(ctx, contracts.ParseRequest{
|
doc, err := adapter.Parse(ctx, contracts.ParseRequest{
|
||||||
SourceID: input.SourceID,
|
SourceID: input.SourceID,
|
||||||
Path: input.Path,
|
Path: input.Path,
|
||||||
Raw: input.RawInput,
|
Raw: input.RawInput,
|
||||||
Metadata: input.Metadata,
|
LLMProfile: input.Pipeline.Input.LLMProfile,
|
||||||
|
Options: cloneOptions(input.Pipeline.Input.Options),
|
||||||
|
Metadata: input.Metadata,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failOutput(output), fmt.Errorf("parse input with adapter %q: %w", adapter.Key(), err)
|
return failOutput(output), fmt.Errorf("parse input with adapter %q: %w", adapter.Key(), err)
|
||||||
@@ -83,8 +85,10 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
|||||||
return failOutput(output), fmt.Errorf("build chunker %q: %w", input.Pipeline.Chunk.Module, err)
|
return failOutput(output), fmt.Errorf("build chunker %q: %w", input.Pipeline.Chunk.Module, err)
|
||||||
}
|
}
|
||||||
chunkResult, err := chunker.Chunk(ctx, contracts.ChunkRequest{
|
chunkResult, err := chunker.Chunk(ctx, contracts.ChunkRequest{
|
||||||
Source: doc,
|
Source: doc,
|
||||||
Metadata: input.Metadata,
|
LLMProfile: input.Pipeline.Chunk.LLMProfile,
|
||||||
|
Options: cloneOptions(input.Pipeline.Chunk.Options),
|
||||||
|
Metadata: input.Metadata,
|
||||||
})
|
})
|
||||||
output.Warnings = append(output.Warnings, chunkResult.Warnings...)
|
output.Warnings = append(output.Warnings, chunkResult.Warnings...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -112,11 +116,13 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
|||||||
return failOutput(output), fmt.Errorf("build output encoder %q: %w", input.Pipeline.Output.Module, err)
|
return failOutput(output), fmt.Errorf("build output encoder %q: %w", input.Pipeline.Output.Module, err)
|
||||||
}
|
}
|
||||||
encoded, err := encoder.Encode(ctx, contracts.OutputRequest{
|
encoded, err := encoder.Encode(ctx, contracts.OutputRequest{
|
||||||
Manifest: output.Manifest,
|
Manifest: output.Manifest,
|
||||||
Approved: output.Approved,
|
Approved: output.Approved,
|
||||||
Rejected: output.Rejected,
|
Rejected: output.Rejected,
|
||||||
Warnings: output.Warnings,
|
Warnings: output.Warnings,
|
||||||
Metadata: input.Metadata,
|
LLMProfile: input.Pipeline.Output.LLMProfile,
|
||||||
|
Options: cloneOptions(input.Pipeline.Output.Options),
|
||||||
|
Metadata: input.Metadata,
|
||||||
})
|
})
|
||||||
output.Warnings = append(output.Warnings, encoded.Warnings...)
|
output.Warnings = append(output.Warnings, encoded.Warnings...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -142,24 +148,28 @@ func (r *Runner) runLane(ctx context.Context, input RunInput, doc *source.Source
|
|||||||
return fmt.Errorf("build normalizer %q for lane %q: %w", lane.Normalize.Module, lane.ID, err)
|
return fmt.Errorf("build normalizer %q for lane %q: %w", lane.Normalize.Module, lane.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var validators []contracts.Validator
|
var validators []validatorExecution
|
||||||
if len(lane.Validators) > 0 {
|
if len(lane.Validators) > 0 {
|
||||||
validators, err = r.buildConfiguredValidators(lane)
|
validators, err = r.buildConfiguredValidators(lane)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
validators = extractor.Validators()
|
for _, validator := range extractor.Validators() {
|
||||||
|
validators = append(validators, validatorExecution{validator: validator})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkArtifacts := make([]contracts.ChunkArtifacts, 0, len(chunks))
|
chunkArtifacts := make([]contracts.ChunkArtifacts, 0, len(chunks))
|
||||||
for index := range chunks {
|
for index := range chunks {
|
||||||
chunk := chunks[index]
|
chunk := chunks[index]
|
||||||
result, err := extractor.Extract(ctx, contracts.ExtractionRequest{
|
result, err := extractor.Extract(ctx, contracts.ExtractionRequest{
|
||||||
Source: doc,
|
Source: doc,
|
||||||
Chunk: &chunk,
|
Chunk: &chunk,
|
||||||
LLMClient: input.LLMClient,
|
LLMClient: input.LLMClient,
|
||||||
Metadata: input.Metadata,
|
LLMProfile: lane.Extract.LLMProfile,
|
||||||
|
Options: cloneOptions(lane.Extract.Options),
|
||||||
|
Metadata: input.Metadata,
|
||||||
})
|
})
|
||||||
output.Warnings = append(output.Warnings, result.Warnings...)
|
output.Warnings = append(output.Warnings, result.Warnings...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,6 +190,8 @@ func (r *Runner) runLane(ctx context.Context, input RunInput, doc *source.Source
|
|||||||
Source: doc,
|
Source: doc,
|
||||||
LaneID: lane.ID,
|
LaneID: lane.ID,
|
||||||
ChunkArtifacts: chunkArtifacts,
|
ChunkArtifacts: chunkArtifacts,
|
||||||
|
LLMProfile: lane.Merge.LLMProfile,
|
||||||
|
Options: cloneOptions(lane.Merge.Options),
|
||||||
Metadata: input.Metadata,
|
Metadata: input.Metadata,
|
||||||
})
|
})
|
||||||
output.Warnings = append(output.Warnings, mergeResult.Warnings...)
|
output.Warnings = append(output.Warnings, mergeResult.Warnings...)
|
||||||
@@ -191,6 +203,8 @@ func (r *Runner) runLane(ctx context.Context, input RunInput, doc *source.Source
|
|||||||
Source: doc,
|
Source: doc,
|
||||||
LaneID: lane.ID,
|
LaneID: lane.ID,
|
||||||
Candidates: mergeResult.Candidates,
|
Candidates: mergeResult.Candidates,
|
||||||
|
LLMProfile: lane.Normalize.LLMProfile,
|
||||||
|
Options: cloneOptions(lane.Normalize.Options),
|
||||||
Metadata: input.Metadata,
|
Metadata: input.Metadata,
|
||||||
})
|
})
|
||||||
output.Warnings = append(output.Warnings, normalizeResult.Warnings...)
|
output.Warnings = append(output.Warnings, normalizeResult.Warnings...)
|
||||||
@@ -198,6 +212,10 @@ func (r *Runner) runLane(ctx context.Context, input RunInput, doc *source.Source
|
|||||||
return fmt.Errorf("normalize lane %q with normalizer %q: %w", lane.ID, normalizer.Key(), err)
|
return fmt.Errorf("normalize lane %q with normalizer %q: %w", lane.ID, normalizer.Key(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateCandidateEnvelope(extractor, normalizeResult.Candidates); err != nil {
|
||||||
|
return fmt.Errorf("validate normalized candidates for lane %q: %w", lane.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
approved, rejected, warnings, err := runValidators(ctx, extractor.Key(), validators, doc, normalizeResult.Candidates, input.Metadata)
|
approved, rejected, warnings, err := runValidators(ctx, extractor.Key(), validators, doc, normalizeResult.Candidates, input.Metadata)
|
||||||
output.Warnings = append(output.Warnings, warnings...)
|
output.Warnings = append(output.Warnings, warnings...)
|
||||||
output.Rejected = append(output.Rejected, rejected...)
|
output.Rejected = append(output.Rejected, rejected...)
|
||||||
@@ -211,14 +229,22 @@ func (r *Runner) runLane(ctx context.Context, input RunInput, doc *source.Source
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) buildConfiguredValidators(lane ResolvedArtifactLane) ([]contracts.Validator, error) {
|
type validatorExecution struct {
|
||||||
validators := make([]contracts.Validator, 0, len(lane.Validators))
|
validator contracts.Validator
|
||||||
|
binding ModuleBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) buildConfiguredValidators(lane ResolvedArtifactLane) ([]validatorExecution, error) {
|
||||||
|
validators := make([]validatorExecution, 0, len(lane.Validators))
|
||||||
for _, binding := range lane.Validators {
|
for _, binding := range lane.Validators {
|
||||||
validator, err := r.registries.Validators.Build(binding.Module)
|
validator, err := r.registries.Validators.Build(binding.Module)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("build validator %q for lane %q: %w", binding.Module, lane.ID, err)
|
return nil, fmt.Errorf("build validator %q for lane %q: %w", binding.Module, lane.ID, err)
|
||||||
}
|
}
|
||||||
validators = append(validators, validator)
|
validators = append(validators, validatorExecution{
|
||||||
|
validator: validator,
|
||||||
|
binding: binding,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return validators, nil
|
return validators, nil
|
||||||
}
|
}
|
||||||
@@ -359,18 +385,51 @@ func normalizeCandidates(extractor contracts.Extractor, candidates []artifacts.A
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runValidators(ctx context.Context, extractorKey string, validators []contracts.Validator, doc *source.SourceDocument, candidates []artifacts.ArtifactCandidate, metadata map[string]any) ([]artifacts.ArtifactCandidate, []artifacts.RejectedArtifact, []contracts.Warning, error) {
|
func validateCandidateEnvelope(extractor contracts.Extractor, candidates []artifacts.ArtifactCandidate) error {
|
||||||
|
seen := make(map[int]struct{}, len(candidates))
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if _, ok := seen[candidate.Index]; ok {
|
||||||
|
return fmt.Errorf("candidate index %d is duplicated", candidate.Index)
|
||||||
|
}
|
||||||
|
seen[candidate.Index] = struct{}{}
|
||||||
|
|
||||||
|
if candidate.ExtractorKey == "" {
|
||||||
|
return fmt.Errorf("candidate index %d extractor_key must not be empty", candidate.Index)
|
||||||
|
}
|
||||||
|
if candidate.ExtractorKey != extractor.Key() {
|
||||||
|
return fmt.Errorf("candidate index %d extractor_key %q does not match extractor %q", candidate.Index, candidate.ExtractorKey, extractor.Key())
|
||||||
|
}
|
||||||
|
if candidate.ArtifactType == "" {
|
||||||
|
return fmt.Errorf("candidate index %d artifact_type must not be empty", candidate.Index)
|
||||||
|
}
|
||||||
|
if candidate.ArtifactType != extractor.ArtifactType() {
|
||||||
|
return fmt.Errorf("candidate index %d artifact_type %q does not match extractor %q artifact type %q", candidate.Index, candidate.ArtifactType, extractor.Key(), extractor.ArtifactType())
|
||||||
|
}
|
||||||
|
if candidate.SchemaVersion == "" {
|
||||||
|
return fmt.Errorf("candidate index %d schema_version must not be empty", candidate.Index)
|
||||||
|
}
|
||||||
|
if candidate.SchemaVersion != extractor.SchemaVersion() {
|
||||||
|
return fmt.Errorf("candidate index %d schema_version %q does not match extractor %q schema version %q", candidate.Index, candidate.SchemaVersion, extractor.Key(), extractor.SchemaVersion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runValidators(ctx context.Context, extractorKey string, validators []validatorExecution, doc *source.SourceDocument, candidates []artifacts.ArtifactCandidate, metadata map[string]any) ([]artifacts.ArtifactCandidate, []artifacts.RejectedArtifact, []contracts.Warning, error) {
|
||||||
eligible := candidates
|
eligible := candidates
|
||||||
var rejected []artifacts.RejectedArtifact
|
var rejected []artifacts.RejectedArtifact
|
||||||
var warnings []contracts.Warning
|
var warnings []contracts.Warning
|
||||||
|
|
||||||
for validatorIndex, validator := range validators {
|
for validatorIndex, execution := range validators {
|
||||||
|
validator := execution.validator
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
return nil, rejected, warnings, fmt.Errorf("extractor %q validator[%d] must not be nil", extractorKey, validatorIndex)
|
return nil, rejected, warnings, fmt.Errorf("extractor %q validator[%d] must not be nil", extractorKey, validatorIndex)
|
||||||
}
|
}
|
||||||
result, err := validator.Validate(ctx, contracts.ValidationRequest{
|
result, err := validator.Validate(ctx, contracts.ValidationRequest{
|
||||||
Source: doc,
|
Source: doc,
|
||||||
Candidates: eligible,
|
Candidates: eligible,
|
||||||
|
LLMProfile: execution.binding.LLMProfile,
|
||||||
|
Options: cloneOptions(execution.binding.Options),
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
})
|
})
|
||||||
warnings = append(warnings, result.Warnings...)
|
warnings = append(warnings, result.Warnings...)
|
||||||
|
|||||||
@@ -311,6 +311,66 @@ func TestRunPassesInputRequestFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunPassesModuleBindingConfigToStageRequests(t *testing.T) {
|
||||||
|
modules := defaultRunnerModules()
|
||||||
|
pipeline := resolvedPipelineWithValidators("configured")
|
||||||
|
pipeline.Input = ModuleBinding{Module: "input", LLMProfile: "input-profile", Options: map[string]any{"input_option": "input-value"}}
|
||||||
|
pipeline.Chunk = ModuleBinding{Module: "chunk", LLMProfile: "chunk-profile", Options: map[string]any{"chunk_option": "chunk-value"}}
|
||||||
|
pipeline.Output = ModuleBinding{Module: "output", LLMProfile: "output-profile", Options: map[string]any{"output_option": "output-value"}}
|
||||||
|
pipeline.ArtifactLanes[0].Extract = ModuleBinding{Module: "extract-alpha", LLMProfile: "extract-profile", Options: map[string]any{"extract_option": "extract-value"}}
|
||||||
|
pipeline.ArtifactLanes[0].Merge = ModuleBinding{Module: "merge", LLMProfile: "merge-profile", Options: map[string]any{"merge_option": "merge-value"}}
|
||||||
|
pipeline.ArtifactLanes[0].Normalize = ModuleBinding{Module: "normalize", LLMProfile: "normalize-profile", Options: map[string]any{"normalize_option": "normalize-value"}}
|
||||||
|
pipeline.ArtifactLanes[0].Validators[0] = ModuleBinding{Module: "configured", LLMProfile: "validator-profile", Options: map[string]any{"validator_option": "validator-value"}}
|
||||||
|
|
||||||
|
_, err := New(newRunnerRegistries(t, modules)).Run(context.Background(), RunInput{Pipeline: pipeline})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := modules.input.requests[0].LLMProfile; got != "input-profile" {
|
||||||
|
t.Fatalf("input LLMProfile = %q, want input-profile", got)
|
||||||
|
}
|
||||||
|
if got := modules.input.requests[0].Options["input_option"]; got != "input-value" {
|
||||||
|
t.Fatalf("input Options = %#v, want input option", modules.input.requests[0].Options)
|
||||||
|
}
|
||||||
|
if got := modules.chunker.requests[0].LLMProfile; got != "chunk-profile" {
|
||||||
|
t.Fatalf("chunk LLMProfile = %q, want chunk-profile", got)
|
||||||
|
}
|
||||||
|
if got := modules.chunker.requests[0].Options["chunk_option"]; got != "chunk-value" {
|
||||||
|
t.Fatalf("chunk Options = %#v, want chunk option", modules.chunker.requests[0].Options)
|
||||||
|
}
|
||||||
|
if got := modules.extractors["extract-alpha"].requests[0].LLMProfile; got != "extract-profile" {
|
||||||
|
t.Fatalf("extract LLMProfile = %q, want extract-profile", got)
|
||||||
|
}
|
||||||
|
if got := modules.extractors["extract-alpha"].requests[0].Options["extract_option"]; got != "extract-value" {
|
||||||
|
t.Fatalf("extract Options = %#v, want extract option", modules.extractors["extract-alpha"].requests[0].Options)
|
||||||
|
}
|
||||||
|
if got := modules.mergers["merge"].requests[0].LLMProfile; got != "merge-profile" {
|
||||||
|
t.Fatalf("merge LLMProfile = %q, want merge-profile", got)
|
||||||
|
}
|
||||||
|
if got := modules.mergers["merge"].requests[0].Options["merge_option"]; got != "merge-value" {
|
||||||
|
t.Fatalf("merge Options = %#v, want merge option", modules.mergers["merge"].requests[0].Options)
|
||||||
|
}
|
||||||
|
if got := modules.normalizers["normalize"].requests[0].LLMProfile; got != "normalize-profile" {
|
||||||
|
t.Fatalf("normalize LLMProfile = %q, want normalize-profile", got)
|
||||||
|
}
|
||||||
|
if got := modules.normalizers["normalize"].requests[0].Options["normalize_option"]; got != "normalize-value" {
|
||||||
|
t.Fatalf("normalize Options = %#v, want normalize option", modules.normalizers["normalize"].requests[0].Options)
|
||||||
|
}
|
||||||
|
if got := modules.validators["configured"].requests[0].LLMProfile; got != "validator-profile" {
|
||||||
|
t.Fatalf("validator LLMProfile = %q, want validator-profile", got)
|
||||||
|
}
|
||||||
|
if got := modules.validators["configured"].requests[0].Options["validator_option"]; got != "validator-value" {
|
||||||
|
t.Fatalf("validator Options = %#v, want validator option", modules.validators["configured"].requests[0].Options)
|
||||||
|
}
|
||||||
|
if got := modules.output.requests[0].LLMProfile; got != "output-profile" {
|
||||||
|
t.Fatalf("output LLMProfile = %q, want output-profile", got)
|
||||||
|
}
|
||||||
|
if got := modules.output.requests[0].Options["output_option"]; got != "output-value" {
|
||||||
|
t.Fatalf("output Options = %#v, want output option", modules.output.requests[0].Options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunPassesPerChunkCandidatesToMergeAndNormalize(t *testing.T) {
|
func TestRunPassesPerChunkCandidatesToMergeAndNormalize(t *testing.T) {
|
||||||
modules := defaultRunnerModules()
|
modules := defaultRunnerModules()
|
||||||
|
|
||||||
@@ -346,6 +406,54 @@ func TestRunPassesPerChunkCandidatesToMergeAndNormalize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunRejectsInvalidPostNormalizeCandidateEnvelope(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
candidates []artifacts.ArtifactCandidate
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "duplicate index",
|
||||||
|
candidates: []artifacts.ArtifactCandidate{
|
||||||
|
runnerCandidate(0),
|
||||||
|
runnerCandidate(0),
|
||||||
|
},
|
||||||
|
want: "duplicated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing extractor key",
|
||||||
|
candidates: []artifacts.ArtifactCandidate{
|
||||||
|
{Index: 0, ArtifactType: "artifact", SchemaVersion: "v1", Payload: []byte(`{"value":true}`)},
|
||||||
|
},
|
||||||
|
want: "extractor_key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatched schema version",
|
||||||
|
candidates: []artifacts.ArtifactCandidate{
|
||||||
|
{Index: 0, ExtractorKey: "extract-alpha", ArtifactType: "artifact", SchemaVersion: "other", Payload: []byte(`{"value":true}`)},
|
||||||
|
},
|
||||||
|
want: "schema_version",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
modules := defaultRunnerModules()
|
||||||
|
modules.normalizers["normalize"].result = test.candidates
|
||||||
|
|
||||||
|
output, err := New(newRunnerRegistries(t, modules)).Run(context.Background(), RunInput{Pipeline: resolvedPipeline()})
|
||||||
|
|
||||||
|
assertRunError(t, err, test.want)
|
||||||
|
if output.Manifest.ValidationStatus != "failed" {
|
||||||
|
t.Fatalf("ValidationStatus = %q, want failed", output.Manifest.ValidationStatus)
|
||||||
|
}
|
||||||
|
if len(output.Approved) != 0 {
|
||||||
|
t.Fatalf("len(Approved) = %d, want no approved artifacts", len(output.Approved))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunValidatorApprovalAndRejection(t *testing.T) {
|
func TestRunValidatorApprovalAndRejection(t *testing.T) {
|
||||||
modules := defaultRunnerModules()
|
modules := defaultRunnerModules()
|
||||||
rejectFirst := &runnerValidator{
|
rejectFirst := &runnerValidator{
|
||||||
@@ -784,6 +892,7 @@ type runnerExtractor struct {
|
|||||||
validators []contracts.Validator
|
validators []contracts.Validator
|
||||||
warnings []contracts.Warning
|
warnings []contracts.Warning
|
||||||
err error
|
err error
|
||||||
|
requests []contracts.ExtractionRequest
|
||||||
seenChunkIDs []string
|
seenChunkIDs []string
|
||||||
seenLLMClients []contracts.StructuredLLMClient
|
seenLLMClients []contracts.StructuredLLMClient
|
||||||
seenMetadata []map[string]any
|
seenMetadata []map[string]any
|
||||||
@@ -806,6 +915,7 @@ func (extractor *runnerExtractor) Validators() []contracts.Validator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (extractor *runnerExtractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error) {
|
func (extractor *runnerExtractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error) {
|
||||||
|
extractor.requests = append(extractor.requests, req)
|
||||||
if req.Chunk != nil {
|
if req.Chunk != nil {
|
||||||
extractor.seenChunkIDs = append(extractor.seenChunkIDs, req.Chunk.ID)
|
extractor.seenChunkIDs = append(extractor.seenChunkIDs, req.Chunk.ID)
|
||||||
}
|
}
|
||||||
@@ -880,6 +990,7 @@ type runnerValidator struct {
|
|||||||
err error
|
err error
|
||||||
order *[]string
|
order *[]string
|
||||||
calls int
|
calls int
|
||||||
|
requests []contracts.ValidationRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (validator *runnerValidator) Name() string {
|
func (validator *runnerValidator) Name() string {
|
||||||
@@ -888,6 +999,7 @@ func (validator *runnerValidator) Name() string {
|
|||||||
|
|
||||||
func (validator *runnerValidator) Validate(ctx context.Context, req contracts.ValidationRequest) (contracts.ValidationResult, error) {
|
func (validator *runnerValidator) Validate(ctx context.Context, req contracts.ValidationRequest) (contracts.ValidationResult, error) {
|
||||||
validator.calls++
|
validator.calls++
|
||||||
|
validator.requests = append(validator.requests, req)
|
||||||
if validator.order != nil {
|
if validator.order != nil {
|
||||||
*validator.order = append(*validator.order, validator.name)
|
*validator.order = append(*validator.order, validator.name)
|
||||||
}
|
}
|
||||||
@@ -981,6 +1093,16 @@ func candidateIndices(candidates []artifacts.ArtifactCandidate) []int {
|
|||||||
return indices
|
return indices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runnerCandidate(index int) artifacts.ArtifactCandidate {
|
||||||
|
return artifacts.ArtifactCandidate{
|
||||||
|
Index: index,
|
||||||
|
ExtractorKey: "extract-alpha",
|
||||||
|
ArtifactType: "artifact",
|
||||||
|
SchemaVersion: "v1",
|
||||||
|
Payload: []byte(`{"value":true}`),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertRunError(t *testing.T, err error, want string) {
|
func assertRunError(t *testing.T, err error, want string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user