535 lines
14 KiB
Markdown
535 lines
14 KiB
Markdown
# Implementation Plan: Checkpoint 1 Core Contracts And Skeleton
|
|
|
|
## Status
|
|
|
|
This is a staged implementation plan for
|
|
[`1-core-contracts-and-skeleton.md`](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:
|
|
|
|
- [`docs/policy/architecture.md`](../policy/architecture.md)
|
|
- [`docs/policy/documentation.md`](../policy/documentation.md)
|
|
|
|
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`:
|
|
|
|
```go
|
|
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:
|
|
|
|
```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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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.
|