Files
notarius/docs/roadmap/implementation.md

14 KiB

Implementation Plan: Checkpoint 1 Core Contracts And Skeleton

Status

This is a staged implementation plan for 1-core-contracts-and-skeleton.md. It is intended for an LLM coding agent to follow stage by stage.

This plan implements only checkpoint 1. Do not implement real input adapters, real extractors, LLM provider clients, prompt assets, response schema registries, diagnostics run directories, or production config loading in this pass.

Policy Context

Follow:

Required boundaries:

  • core packages must stay source-agnostic and domain-agnostic;
  • transcript-specific concepts must not appear in core or framework contracts;
  • D&D concepts must not appear in core or framework contracts;
  • source references must target generic source units;
  • planned behavior outside this checkpoint must remain in roadmap docs.

Global Implementation Decisions

  • Use module path gitea.maximumdirect.net/eric/notarius.
  • Use Go 1.24.0, matching the nearby Audita project.
  • Add no third-party dependencies in checkpoint 1.
  • Keep CLI behavior minimal: root help/usage and unknown-command handling only.
  • Use package names exactly as listed in this plan unless implementation reveals a compile-time conflict.
  • Use encoding/json.RawMessage for generic artifact payloads.
  • Store contract tests close to the contracts they exercise.
  • Run gofmt on all Go files before validation.

Stage 1: Bootstrap Go Project And CLI Shell

Goal

Create a compileable Go application shell with a minimal CLI entrypoint.

Files To Add

  • go.mod
  • cmd/notarius/main.go
  • internal/cli/run.go
  • internal/cli/run_test.go

Required Implementation

Create go.mod:

module gitea.maximumdirect.net/eric/notarius

go 1.24.0

Create cmd/notarius/main.go:

  • import os;
  • import gitea.maximumdirect.net/eric/notarius/internal/cli;
  • call os.Exit(cli.Run(os.Args[1:], os.Stdout, os.Stderr)).

Create internal/cli.Run(args []string, stdout, stderr io.Writer) int.

Initial behavior:

  • notarius, notarius help, notarius --help, and notarius -h write root usage to stdout and return 0;
  • unknown commands write notarius: unknown command "<cmd>" plus usage to stderr and return 2;
  • no extract command is implemented in checkpoint 1.

Root usage should be short and should not document unimplemented behavior as if it exists. Acceptable text:

Usage:
  notarius help

Required Tests

In internal/cli/run_test.go, cover:

  • no args returns 0 and writes usage to stdout;
  • help, --help, and -h return 0;
  • unknown command returns 2, writes to stderr, and does not write meaningful stdout.

Validation

Run:

gofmt -w cmd internal
go test ./...
go build ./cmd/notarius

Stage 2: Core Source Model

Goal

Add the generic source-document model and validation helpers.

Files To Add

  • internal/core/source/source.go
  • internal/core/source/validation.go
  • internal/core/source/source_test.go

Required Types

Create package source.

Define:

type SourceDocument struct {
    ID       string         `json:"id"`
    Kind     string         `json:"kind"`
    Format   string         `json:"format"`
    Digest   string         `json:"digest"`
    Units    []SourceUnit   `json:"units"`
    Metadata map[string]any `json:"metadata,omitempty"`
}

type SourceUnit struct {
    ID       string         `json:"id"`
    Kind     string         `json:"kind"`
    Text     string         `json:"text"`
    Metadata map[string]any `json:"metadata,omitempty"`
}

type SourceRef struct {
    SourceID    string `json:"source_id"`
    StartUnitID string `json:"start_unit_id"`
    EndUnitID   string `json:"end_unit_id"`
}

Required Helpers

Implement:

func ValidateDocument(doc *SourceDocument) error
func ValidateRef(doc *SourceDocument, ref SourceRef) error
func UnitIndex(doc *SourceDocument, unitID string) (int, bool)

Validation rules:

  • document must be non-nil;
  • document ID, Kind, Format, and Digest must be non-empty after trimming whitespace;
  • document must contain at least one unit;
  • each unit ID, Kind, and Text must be non-empty after trimming whitespace;
  • unit IDs must be unique within a document;
  • ValidateRef requires non-empty SourceID, StartUnitID, and EndUnitID;
  • ValidateRef requires ref.SourceID == doc.ID;
  • start and end unit IDs must exist;
  • start unit index must be less than or equal to end unit index.

Do not add transcript-specific fields or helpers.

Error Style

Return ordinary Go errors with actionable field context, such as:

  • source document id must not be empty;
  • source unit[2].id must not be empty;
  • source unit id "u1" is duplicated;
  • source ref start_unit_id "u9" was not found.

Required Tests

Cover:

  • valid document;
  • nil document;
  • missing document fields;
  • empty units;
  • missing unit fields;
  • duplicate unit IDs;
  • valid source reference;
  • source ID mismatch;
  • missing source-ref unit ID;
  • reversed source-ref unit order.

Validation

Run:

gofmt -w internal/core/source
go test ./internal/core/source
go test ./...

Stage 3: Core Artifact Model

Goal

Add extractor-neutral artifact and manifest types.

Files To Add

  • internal/core/artifacts/artifacts.go
  • internal/core/artifacts/artifacts_test.go

Required Types

Create package artifacts.

Define:

type Candidate struct {
    Index         int                 `json:"index"`
    ExtractorKey  string              `json:"extractor_key"`
    ArtifactType  string              `json:"artifact_type"`
    SchemaVersion string              `json:"schema_version"`
    Payload       json.RawMessage     `json:"payload"`
    SourceRefs    []source.SourceRef  `json:"source_refs,omitempty"`
    Metadata      map[string]any      `json:"metadata,omitempty"`
}

type Artifact struct {
    ExtractorKey  string             `json:"extractor_key"`
    ArtifactType  string             `json:"artifact_type"`
    SchemaVersion string             `json:"schema_version"`
    Payload       json.RawMessage    `json:"payload"`
    SourceRefs    []source.SourceRef `json:"source_refs,omitempty"`
    Metadata      map[string]any     `json:"metadata,omitempty"`
}

type RejectedArtifact struct {
    Candidate     Candidate `json:"candidate"`
    ValidatorName string    `json:"validator_name"`
    ReasonCode    string    `json:"reason_code"`
    Message       string    `json:"message"`
}

type RunManifest struct {
    RunID            string     `json:"run_id,omitempty"`
    InputAdapter     string     `json:"input_adapter,omitempty"`
    SourceDigests    []string   `json:"source_digests,omitempty"`
    Extractors       []string   `json:"extractors,omitempty"`
    SchemaVersion    string     `json:"schema_version,omitempty"`
    ValidationStatus string     `json:"validation_status,omitempty"`
    StartedAt        *time.Time `json:"started_at,omitempty"`
    CompletedAt      *time.Time `json:"completed_at,omitempty"`
}

Imports should include:

  • encoding/json;
  • time;
  • gitea.maximumdirect.net/eric/notarius/internal/core/source.

Add:

func ArtifactFromCandidate(candidate Candidate) Artifact

This helper should copy candidate fields into an approved artifact. It may share immutable json.RawMessage bytes at this checkpoint, but copying slices is preferred where cheap.

Required Tests

Cover:

  • ArtifactFromCandidate preserves extractor key, artifact type, schema version, payload, source refs, and metadata;
  • JSON marshaling uses the expected field names;
  • empty optional manifest fields are omitted.

Do not add D&D-specific payload structs.

Validation

Run:

gofmt -w internal/core/artifacts
go test ./internal/core/artifacts
go test ./...

Stage 4: Framework Contract Types

Goal

Define the interfaces and request/response types later checkpoints will build against.

Files To Add

  • internal/framework/contracts/contracts.go
  • internal/framework/contracts/contracts_test.go

Required Types And Interfaces

Create package contracts.

Define LLM-neutral message and completion types:

type LLMMessage struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type StructuredCompletionRequest struct {
    StageName          string          `json:"stage_name"`
    Messages           []LLMMessage    `json:"messages"`
    Model              string          `json:"model,omitempty"`
    ResponseSchemaName string          `json:"response_schema_name,omitempty"`
    ResponseSchema     json.RawMessage `json:"response_schema,omitempty"`
}

type StructuredCompletionResponse struct {
    Content          json.RawMessage `json:"content"`
    Provider         string          `json:"provider,omitempty"`
    Model            string          `json:"model,omitempty"`
    PromptTokens     int             `json:"prompt_tokens,omitempty"`
    CompletionTokens int             `json:"completion_tokens,omitempty"`
    TotalTokens      int             `json:"total_tokens,omitempty"`
}

type StructuredLLMClient interface {
    CompleteStructured(ctx context.Context, req StructuredCompletionRequest, out any) (StructuredCompletionResponse, error)
}

Define input adapter contract:

type ParseRequest struct {
    SourceID string         `json:"source_id,omitempty"`
    Path     string         `json:"path,omitempty"`
    Raw      []byte         `json:"-"`
    Metadata map[string]any `json:"metadata,omitempty"`
}

type InputAdapter interface {
    Key() string
    Parse(ctx context.Context, req ParseRequest) (*source.SourceDocument, error)
}

Define extractor contract:

type ExtractionRequest struct {
    Source    *source.SourceDocument `json:"-"`
    LLMClient StructuredLLMClient    `json:"-"`
    Metadata  map[string]any         `json:"metadata,omitempty"`
}

type ExtractionResult struct {
    Candidates []artifacts.Candidate `json:"candidates,omitempty"`
    Warnings   []Warning             `json:"warnings,omitempty"`
}

type Extractor interface {
    Key() string
    ArtifactType() string
    SchemaVersion() string
    Validators() []Validator
    Extract(ctx context.Context, req ExtractionRequest) (ExtractionResult, error)
}

Define validator contract:

type ValidationRequest struct {
    Source     *source.SourceDocument  `json:"-"`
    Candidates []artifacts.Candidate   `json:"candidates"`
    Metadata   map[string]any          `json:"metadata,omitempty"`
}

type ValidationDecision struct {
    CandidateIndex         int    `json:"candidate_index"`
    Approved               bool   `json:"approved"`
    ReasonCode             string `json:"reason_code"`
    Message                string `json:"message"`
    DiagnosticArtifactPath string `json:"diagnostic_artifact_path,omitempty"`
}

type ValidationResult struct {
    ValidatorName string               `json:"validator_name"`
    Decisions     []ValidationDecision `json:"decisions"`
    Warnings      []Warning            `json:"warnings,omitempty"`
}

type Validator interface {
    Name() string
    Validate(ctx context.Context, req ValidationRequest) (ValidationResult, error)
}

Define shared warning type:

type Warning struct {
    Scope      string `json:"scope,omitempty"`
    ReasonCode string `json:"reason_code"`
    Message    string `json:"message"`
}

Required Tests

Use compile-time interface assertions with fake implementations:

var _ InputAdapter = fakeAdapter{}
var _ Extractor = fakeExtractor{}
var _ Validator = fakeValidator{}
var _ StructuredLLMClient = fakeLLMClient{}

Also test that a fake extractor can return a candidate and expose a validator without importing concrete adapter or extractor packages.

Validation

Run:

gofmt -w internal/framework/contracts
go test ./internal/framework/contracts
go test ./...

Stage 5: Cross-Package Contract Composition Tests

Goal

Prove the checkpoint 1 contracts compose at a minimal end-to-end type level without implementing checkpoint 2 runner behavior.

Files To Add

  • internal/framework/contracts/composition_test.go

This can live in package contracts_test to verify public contracts from the outside.

Required Test Scenario

Create fakes in the test:

  1. fake adapter parses a ParseRequest into a valid SourceDocument;
  2. fake extractor receives that source and returns one artifacts.Candidate;
  3. fake validator approves that candidate;
  4. test manually calls those fakes in sequence.

Assertions:

  • parsed source passes source.ValidateDocument;
  • candidate source refs pass source.ValidateRef;
  • validator returns one approved decision for the candidate index;
  • no test type or assertion depends on transcript or D&D concepts.

Do not add a registry or runner in this stage. Those belong to checkpoint 2.

Validation

Run:

gofmt -w internal/framework/contracts
go test ./internal/framework/contracts
go test ./...
go build ./cmd/notarius

Stage 6: Final Checkpoint 1 Review Pass

Goal

Clean up naming, formatting, and accidental scope creep before checkpoint 1 is considered complete.

Required Review

Check:

  • no package names mention transcripts, Seriatim, D&D, spells, NPCs, items, or combat;
  • no concrete adapter or extractor package exists;
  • no LLM provider code exists;
  • no third-party dependency was added;
  • README.md was not updated to describe unimplemented behavior;
  • roadmap docs remain the only place describing future behavior.

Required Validation

Run:

gofmt -w cmd internal
go test ./...
go build ./cmd/notarius
git status --short

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 1.

The plan intentionally chooses a narrow generic model and fake-only composition tests. More complete orchestration, registries, real adapters, real extractors, and LLM runtime behavior are deferred to later checkpoint implementation plans.