568 lines
16 KiB
Markdown
568 lines
16 KiB
Markdown
# Implementation Plan: Checkpoint 2 Framework Composition
|
|
|
|
## Status
|
|
|
|
This is a staged implementation plan for
|
|
[`2-framework-composition.md`](2-framework-composition.md). It is intended for
|
|
an LLM coding agent to follow stage by stage.
|
|
|
|
This plan implements only checkpoint 2. Do not implement real input modules,
|
|
real extract modules, source chunking, LLM provider clients, prompt assets,
|
|
response schema registries, diagnostics run directories, production config
|
|
loading, or D&D artifact schemas in this pass.
|
|
|
|
## Policy Context
|
|
|
|
Follow:
|
|
|
|
- [`docs/policy/architecture.md`](../policy/architecture.md)
|
|
- [`docs/policy/documentation.md`](../policy/documentation.md)
|
|
|
|
Required boundaries:
|
|
|
|
- framework packages must stay source-agnostic and domain-agnostic;
|
|
- input adapter registry code must not import concrete input module packages;
|
|
- extractor registry and runner code must not import concrete extract module
|
|
packages;
|
|
- runner code should operate on `SourceDocument`, not transcript-specific
|
|
structures;
|
|
- validators should be independently testable and composable;
|
|
- planned behavior outside this checkpoint must remain in roadmap docs.
|
|
|
|
## Global Implementation Decisions
|
|
|
|
- Add no third-party dependencies in checkpoint 2.
|
|
- Do not change public core source/artifact/contract types unless a stage cannot
|
|
compile without a small compatibility adjustment.
|
|
- Use constructor-based registries. Constructors can close over future
|
|
dependencies without changing registry APIs.
|
|
- Registries normalize keys with `strings.TrimSpace`.
|
|
- Registries reject empty keys, duplicate keys, nil constructors, nil built
|
|
instances, and key/name mismatches.
|
|
- The minimal runner receives an already parsed `*source.SourceDocument`.
|
|
- The minimal runner does not use input adapters yet; input adapter registry is
|
|
tested directly.
|
|
- The runner assigns global candidate indices in extractor execution order.
|
|
- If a candidate supplies non-empty extractor metadata that conflicts with its
|
|
owning extractor, the runner returns an error.
|
|
- If a candidate leaves extractor metadata empty, the runner fills
|
|
`ExtractorKey`, `ArtifactType`, and `SchemaVersion` from the owning extractor.
|
|
- Validator chains run in the order returned by `Extractor.Validators()`.
|
|
- A validator result must use the same `ValidatorName` as `Validator.Name()`.
|
|
- Each validator must return exactly one decision for each currently eligible
|
|
candidate.
|
|
- If an extractor has no validators, its candidates are approved.
|
|
- On setup, extraction, or validation errors, return the current partial
|
|
`RunOutput` with a wrapped error.
|
|
- Run `gofmt` on all touched Go files before validation.
|
|
|
|
## Stage 1: Input Adapter Registry
|
|
|
|
### Goal
|
|
|
|
Add a constructor-based registry for `contracts.InputAdapter`.
|
|
|
|
### Files To Add
|
|
|
|
- `internal/framework/pipeline/input_registry.go`
|
|
- `internal/framework/pipeline/input_registry_test.go`
|
|
|
|
### Required API
|
|
|
|
Extend package `pipeline`.
|
|
|
|
Define:
|
|
|
|
```go
|
|
type InputAdapterConstructor func() (contracts.InputAdapter, error)
|
|
|
|
type InputAdapterRegistry struct {
|
|
// unexported fields
|
|
}
|
|
|
|
func NewInputAdapterRegistry() *InputAdapterRegistry
|
|
func (r *InputAdapterRegistry) Register(key string, constructor InputAdapterConstructor) error
|
|
func (r *InputAdapterRegistry) Build(key string) (contracts.InputAdapter, error)
|
|
func (r *InputAdapterRegistry) RegisteredKeys() []string
|
|
```
|
|
|
|
### Required Behavior
|
|
|
|
`Register` must:
|
|
|
|
- return an error if the registry is nil;
|
|
- trim `key`;
|
|
- reject empty keys;
|
|
- reject nil constructors;
|
|
- reject duplicate keys;
|
|
- store constructors by normalized key.
|
|
|
|
`Build` must:
|
|
|
|
- return an error if the registry is nil;
|
|
- trim `key`;
|
|
- reject empty keys;
|
|
- return a clear unknown-key error for unregistered keys;
|
|
- call the constructor;
|
|
- wrap constructor errors with key context;
|
|
- reject nil adapters;
|
|
- reject adapters whose `Key()` does not match the normalized key.
|
|
|
|
`RegisteredKeys` must:
|
|
|
|
- return nil for a nil registry;
|
|
- return registered keys in sorted order;
|
|
- return a copy that callers cannot mutate to affect registry state.
|
|
|
|
### Required Tests
|
|
|
|
Cover:
|
|
|
|
- successful registration and build;
|
|
- key trimming;
|
|
- empty key rejection;
|
|
- duplicate key rejection;
|
|
- nil constructor rejection;
|
|
- unknown key build error;
|
|
- constructor error wrapping;
|
|
- nil adapter rejection;
|
|
- adapter key mismatch rejection;
|
|
- sorted `RegisteredKeys`;
|
|
- nil registry behavior.
|
|
|
|
Use fake adapters only. Do not add concrete input module packages.
|
|
|
|
### Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/framework/pipeline
|
|
go test ./internal/framework/pipeline
|
|
go test ./...
|
|
```
|
|
|
|
## Stage 2: Extractor Registry
|
|
|
|
### Goal
|
|
|
|
Add a constructor-based registry for `contracts.Extractor`.
|
|
|
|
### Files To Add
|
|
|
|
- `internal/framework/pipeline/extractor_registry.go`
|
|
- `internal/framework/pipeline/extractor_registry_test.go`
|
|
|
|
### Required API
|
|
|
|
Extend package `pipeline`.
|
|
|
|
Define:
|
|
|
|
```go
|
|
type ExtractorConstructor func() (contracts.Extractor, error)
|
|
|
|
type ExtractorRegistry struct {
|
|
// unexported fields
|
|
}
|
|
|
|
func NewExtractorRegistry() *ExtractorRegistry
|
|
func (r *ExtractorRegistry) Register(key string, constructor ExtractorConstructor) error
|
|
func (r *ExtractorRegistry) Build(key string) (contracts.Extractor, error)
|
|
func (r *ExtractorRegistry) RegisteredKeys() []string
|
|
```
|
|
|
|
### Required Behavior
|
|
|
|
Mirror input adapter registry behavior, but for `contracts.Extractor`.
|
|
|
|
`Build` must reject extractors whose `Key()` does not match the normalized key.
|
|
It should not validate artifact type, schema version, or validator chain. Those
|
|
are extractor/runtime concerns.
|
|
|
|
### Required Tests
|
|
|
|
Cover the same cases as the input adapter registry:
|
|
|
|
- successful registration and build;
|
|
- key trimming;
|
|
- empty key rejection;
|
|
- duplicate key rejection;
|
|
- nil constructor rejection;
|
|
- unknown key build error;
|
|
- constructor error wrapping;
|
|
- nil extractor rejection;
|
|
- extractor key mismatch rejection;
|
|
- sorted `RegisteredKeys`;
|
|
- nil registry behavior.
|
|
|
|
Use fake extractors only. Do not add concrete extract module packages.
|
|
|
|
### Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/framework/pipeline
|
|
go test ./internal/framework/pipeline
|
|
go test ./...
|
|
```
|
|
|
|
## Stage 3: Validator Runtime Helpers
|
|
|
|
### Goal
|
|
|
|
Add shared validator decision helpers and decision-cardinality enforcement.
|
|
|
|
### Files To Add
|
|
|
|
- `internal/framework/validate/validate.go`
|
|
- `internal/framework/validate/validate_test.go`
|
|
|
|
### Required API
|
|
|
|
Create package `validate`.
|
|
|
|
Define reason constants:
|
|
|
|
```go
|
|
const (
|
|
ReasonApproved = "approved"
|
|
)
|
|
```
|
|
|
|
Define helpers:
|
|
|
|
```go
|
|
func Approved(candidateIndex int) contracts.ValidationDecision
|
|
func Rejected(candidateIndex int, reasonCode string, message string) contracts.ValidationDecision
|
|
func EnforceDecisionCardinality(candidates []artifacts.ArtifactCandidate, decisions []contracts.ValidationDecision) error
|
|
```
|
|
|
|
### Required Behavior
|
|
|
|
`Approved` returns an approved decision with:
|
|
|
|
- `CandidateIndex` set to the input;
|
|
- `Approved` set to true;
|
|
- `ReasonCode` set to `ReasonApproved`;
|
|
- `Message` set to `approved`.
|
|
|
|
`Rejected` returns a rejected decision with:
|
|
|
|
- `CandidateIndex` set to the input;
|
|
- `Approved` set to false;
|
|
- `ReasonCode` set to the trimmed reason code;
|
|
- `Message` set to the trimmed message.
|
|
|
|
`EnforceDecisionCardinality` must:
|
|
|
|
- require exactly one decision per candidate index;
|
|
- reject decisions for unknown candidate indices;
|
|
- reject duplicate decisions for the same candidate index;
|
|
- reject missing decisions;
|
|
- work with candidate indices, not slice positions;
|
|
- allow an empty candidate slice only when decisions are also empty.
|
|
|
|
Artifact candidate indices are expected to be unique by the time validators run.
|
|
If the candidate slice itself contains duplicate indices, return an error.
|
|
|
|
### Required Tests
|
|
|
|
Cover:
|
|
|
|
- approved helper shape;
|
|
- rejected helper trims reason/message;
|
|
- cardinality success with non-zero candidate indices;
|
|
- empty candidates and empty decisions success;
|
|
- unknown decision index error;
|
|
- duplicate decision index error;
|
|
- missing decision index error;
|
|
- duplicate candidate index error.
|
|
|
|
### Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/framework/validate
|
|
go test ./internal/framework/validate
|
|
go test ./...
|
|
```
|
|
|
|
## Stage 4: Minimal Runner Types And Constructor
|
|
|
|
### Goal
|
|
|
|
Add pipeline runner types and constructor without implementing the full run loop
|
|
yet.
|
|
|
|
### Files To Add
|
|
|
|
- `internal/framework/pipeline/runner.go`
|
|
- `internal/framework/pipeline/runner_test.go`
|
|
|
|
### Required API
|
|
|
|
Create or extend package `pipeline`.
|
|
|
|
Define:
|
|
|
|
```go
|
|
type ExtractorFactory interface {
|
|
Build(key string) (contracts.Extractor, error)
|
|
}
|
|
|
|
type Runner struct {
|
|
// unexported fields
|
|
}
|
|
|
|
func New(extractors ExtractorFactory) *Runner
|
|
```
|
|
|
|
Define input/output structs:
|
|
|
|
```go
|
|
type RunInput struct {
|
|
Source *source.SourceDocument
|
|
ExtractorKeys []string
|
|
LLMClient contracts.StructuredLLMClient
|
|
Metadata map[string]any
|
|
}
|
|
|
|
type RunOutput struct {
|
|
Approved []artifacts.Artifact `json:"approved,omitempty"`
|
|
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
|
|
Warnings []contracts.Warning `json:"warnings,omitempty"`
|
|
}
|
|
```
|
|
|
|
Define package errors as ordinary returned errors. Do not introduce custom error
|
|
types in this checkpoint unless tests require behavior that ordinary errors
|
|
cannot express.
|
|
|
|
### Required Behavior
|
|
|
|
At this stage, `New` should only store the extractor factory.
|
|
|
|
If you add `Run` in this stage as a stub, it must return an explicit
|
|
`runner is not implemented` error and must be completed in stage 5. Prefer
|
|
adding the real `Run` method only in stage 5.
|
|
|
|
### Required Tests
|
|
|
|
Cover:
|
|
|
|
- `New` accepts a fake extractor factory and returns a non-nil runner;
|
|
- `RunInput` and `RunOutput` can be constructed with the expected fields.
|
|
|
|
Do not add registry imports to runner tests unless needed for interface
|
|
assertions. Runner should depend only on framework contracts and core packages.
|
|
|
|
### Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/framework/pipeline
|
|
go test ./internal/framework/pipeline
|
|
go test ./...
|
|
```
|
|
|
|
## Stage 5: Minimal Runner Execution
|
|
|
|
### Goal
|
|
|
|
Implement the minimal runner flow from `SourceDocument` to approved/rejected
|
|
artifacts.
|
|
|
|
### Files To Update
|
|
|
|
- `internal/framework/pipeline/runner.go`
|
|
- `internal/framework/pipeline/runner_test.go`
|
|
|
|
### Required API
|
|
|
|
Add:
|
|
|
|
```go
|
|
func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error)
|
|
```
|
|
|
|
### Required Behavior
|
|
|
|
`Run` must:
|
|
|
|
1. reject nil runner or nil extractor factory;
|
|
2. validate `input.Source` with `source.ValidateDocument`;
|
|
3. reject an empty `ExtractorKeys` list;
|
|
4. iterate extractor keys in input order;
|
|
5. build each extractor through the factory;
|
|
6. call `Extractor.Extract` with the source, LLM client, and metadata;
|
|
7. append extractor warnings to `RunOutput.Warnings`;
|
|
8. normalize candidate metadata for each returned candidate;
|
|
9. run validators in `Extractor.Validators()` order;
|
|
10. approve candidates that survive all validators;
|
|
11. reject candidates denied by validators;
|
|
12. return approved/rejected artifacts in deterministic order.
|
|
|
|
Artifact candidate normalization must:
|
|
|
|
- assign `ArtifactCandidate.Index` using a global monotonically increasing counter;
|
|
- fill empty `ExtractorKey`, `ArtifactType`, and `SchemaVersion` from the
|
|
owning extractor;
|
|
- return an error if a candidate's non-empty `ExtractorKey` differs from
|
|
`Extractor.Key()`;
|
|
- return an error if a candidate's non-empty `ArtifactType` differs from
|
|
`Extractor.ArtifactType()`;
|
|
- return an error if a candidate's non-empty `SchemaVersion` differs from
|
|
`Extractor.SchemaVersion()`.
|
|
|
|
Validation behavior:
|
|
|
|
- if an extractor has no validators, all normalized candidates are approved;
|
|
- each validator receives only currently eligible candidates;
|
|
- each validator result must have `ValidatorName == Validator.Name()`;
|
|
- call `validate.EnforceDecisionCardinality` for every validator result;
|
|
- approved decisions keep candidates eligible for the next validator;
|
|
- rejected decisions create `artifacts.RejectedArtifact` records using the
|
|
candidate, validator name, reason code, and message;
|
|
- candidates rejected by one validator are not passed to later validators;
|
|
- append validator warnings to `RunOutput.Warnings`.
|
|
|
|
Error behavior:
|
|
|
|
- return current partial `RunOutput` plus an error for setup, extraction,
|
|
validation, cardinality, or metadata mismatch failures;
|
|
- wrap errors with enough context to identify the extractor key or validator
|
|
name.
|
|
|
|
Do not validate source references inside the runner in checkpoint 2. Source
|
|
reference validation will be a deterministic validator in a later checkpoint.
|
|
|
|
### Required Tests
|
|
|
|
Cover:
|
|
|
|
- nil runner/factory error;
|
|
- invalid source document error;
|
|
- empty extractor list error;
|
|
- extractor keys run in configured order;
|
|
- runner assigns global candidate indices across extractors;
|
|
- runner fills empty candidate extractor metadata;
|
|
- candidate extractor-key mismatch errors;
|
|
- candidate artifact-type mismatch errors;
|
|
- candidate schema-version mismatch errors;
|
|
- no validators approves all candidates;
|
|
- validator approval produces approved artifacts;
|
|
- validator rejection produces rejected artifacts and removes candidate from
|
|
later validators;
|
|
- validator name mismatch errors;
|
|
- validator cardinality error is surfaced;
|
|
- extractor warnings and validator warnings are collected;
|
|
- partial output is returned when a later extractor or validator fails.
|
|
|
|
Use fake extractors, validators, and factories only.
|
|
|
|
### Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/framework/pipeline
|
|
go test ./internal/framework/pipeline
|
|
go test ./...
|
|
```
|
|
|
|
## Stage 6: Registry And Runner Integration Tests
|
|
|
|
### Goal
|
|
|
|
Prove the extractor registry and runner compose without adding real extract
|
|
modules.
|
|
|
|
### Files To Add
|
|
|
|
- `internal/framework/pipeline/registry_integration_test.go`
|
|
|
|
### Required Test Scenario
|
|
|
|
Use `NewExtractorRegistry()` to register two fake extractor constructors.
|
|
|
|
Run the runner with:
|
|
|
|
- a valid `SourceDocument`;
|
|
- extractor keys in a deliberate order;
|
|
- fake extractors that each return one candidate;
|
|
- fake validators that approve or reject candidates.
|
|
|
|
Assertions:
|
|
|
|
- registered fake extractors are built by key;
|
|
- extractor execution follows configured key order;
|
|
- approved artifacts are in deterministic order;
|
|
- rejected artifacts are in deterministic order;
|
|
- no concrete input module or extract module packages are imported.
|
|
|
|
### Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/framework/pipeline
|
|
go test ./internal/framework/pipeline
|
|
go test ./...
|
|
go build ./cmd/notarius
|
|
```
|
|
|
|
## Stage 7: Final Checkpoint 2 Review Pass
|
|
|
|
### Goal
|
|
|
|
Clean up naming, formatting, and accidental scope creep before checkpoint 2 is
|
|
considered complete.
|
|
|
|
### Required Review
|
|
|
|
Check:
|
|
|
|
- no package names mention transcripts, Seriatim, D&D, spells, NPCs, items, or
|
|
combat;
|
|
- no concrete input module or extract module package exists;
|
|
- no LLM provider code exists;
|
|
- no prompt, response schema, diagnostics, config, or source chunking package
|
|
was added;
|
|
- no third-party dependency was added;
|
|
- runner operates on `SourceDocument`;
|
|
- input adapter registry is not wired into runner yet;
|
|
- roadmap docs remain the only place describing future behavior.
|
|
|
|
### Required Validation
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/framework
|
|
go test ./...
|
|
go build ./cmd/notarius
|
|
git status --short
|
|
```
|
|
|
|
Remove the root `./notarius` binary produced by `go build ./cmd/notarius` before
|
|
finishing the implementation turn.
|
|
|
|
The implementation response for this checkpoint should summarize:
|
|
|
|
- files added;
|
|
- tests run;
|
|
- any deviations from this plan and why.
|
|
|
|
## Open Questions
|
|
|
|
No blocking open questions remain for checkpoint 2.
|
|
|
|
The plan intentionally keeps raw input parsing outside the runner. The input
|
|
adapter registry is added and tested now because it is part of framework
|
|
composition, but concrete parsing and adapter-runner wiring remain deferred
|
|
until the Seriatim adapter checkpoint.
|