Files
notarius/docs/roadmap/implementation.md

16 KiB

Implementation Plan: Checkpoint 2 Framework Composition

Status

This is a staged implementation plan for 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:

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:

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:

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:

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:

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:

const (
    ReasonApproved = "approved"
)

Define helpers:

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:

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:

type ExtractorFactory interface {
    Build(key string) (contracts.Extractor, error)
}

type Runner struct {
    // unexported fields
}

func New(extractors ExtractorFactory) *Runner

Define input/output structs:

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:

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:

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:

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:

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:

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.