Plan D&D spells extractor implementation

This commit is contained in:
2026-07-03 23:25:32 +00:00
parent 39fcfba605
commit a4d64f6f16
2 changed files with 482 additions and 339 deletions

View File

@@ -23,7 +23,7 @@ In scope:
- module metadata/capability requirements for pipeline validation; - module metadata/capability requirements for pipeline validation;
- source-reference and schema validators in the extractor chain; - source-reference and schema validators in the extractor chain;
- fake LLM tests; - fake LLM tests;
- CLI-level integration test if the CLI path is ready. - runner-level integration tests with fake infrastructure.
Out of scope: Out of scope:
@@ -31,89 +31,71 @@ Out of scope:
- NPC extraction; - NPC extraction;
- combat extraction; - combat extraction;
- cross-slice deduplication beyond simple deterministic merging; - cross-slice deduplication beyond simple deterministic merging;
- broad D&D rules validation. - broad D&D rules validation;
- a `notarius run` command.
## Proposed Stages ## Target End State
### Stage 1: Spell Artifact Schema The repository should contain a real D&D spells extract-stage module at
`internal/modules/extract/dnd/spells`.
Define the D&D spell artifact model. The spells module should be registered under the stable extractor key
`dnd/spells`. It should be selectable as a named artifact lane in pipeline
configuration, for example a lane named `spells` whose extractor module is
`dnd/spells`.
Initial shape: The module should translate generic source chunks into spell-cast artifact
candidates with this spell payload:
```go - `caster`;
type SpellCast struct { - `spell`;
Player string `json:"player"` - `effect`;
Spell string `json:"spell"` - `narrative_description`.
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
SourceRefs []SourceRef `json:"source_refs"`
}
```
Keep this schema inside the D&D spells extract module or a D&D artifact package, The LLM structured response should also include source references for each
not inside core framework packages. spell cast. The extractor should copy those references into the generic
artifact envelope rather than duplicating source references inside the durable
spell payload.
### Stage 2: Structured Response Schema The caster is the in-world character or creature casting the spell, not the
table speaker. Speaker metadata from transcript source units may be used as
Add a structured response schema asset for spell extraction. optional prompt context when present, but it should not be a required or durable
field in the spell payload.
The schema should require:
- spell-cast array;
- non-empty player, spell, effect, and narrative description fields;
- at least one source reference per spell cast.
### Stage 3: Prompt Assets
Add embedded prompt assets for D&D spell extraction.
Prompts should: Prompts should:
- describe the generic source-unit input format; - describe the generic source-unit input format;
- explain that source references must use source-unit IDs; - explain that source references must use source-unit IDs exactly;
- avoid relying on transcript-specific fields except as optional metadata; - avoid relying on transcript-specific fields except as optional metadata;
- request only spell-cast artifacts. - request only D&D spell-cast artifacts.
### Stage 4: Process Module Implementation The extractor should attach deterministic validators by default. Validation
should cover:
Implement `internal/modules/extract/dnd/spells`. - spell payload shape;
- non-empty required spell fields;
- at least one source reference per spell cast;
- source references that validate against the source document.
The extractor should: The module should declare flat capabilities for pipeline validation. Initial
capabilities should require chunked transcript source material and provide a
D&D spell-cast artifact capability.
- satisfy the framework `Extractor` contract; Implementation staging belongs in
- declare module metadata for pipeline-profile validation; [`implementation.md`](implementation.md).
- build LLM messages from a source document or source chunk;
- call the structured LLM client;
- return artifact candidates with source references;
- attach its validator chain.
### Stage 5: Validators And Tests ## Fixtures And Tests
Wire deterministic validators: The checkpoint should add synthetic fixtures and focused tests for:
- schema/shape validation; - successful spell extraction with a fake structured LLM client;
- source-reference validation; - empty spell-cast results;
- required-field validation if not covered by schema handling.
Add tests using a fake structured LLM client:
- successful spell extraction;
- empty result;
- invalid source reference rejection;
- malformed structured output handling; - malformed structured output handling;
- stable output ordering. - invalid source-reference rejection;
- stable output ordering;
### Stage 6: CLI Integration - pipeline-profile selection of a `spells` artifact lane;
- runner integration from Seriatim input through the spells extractor using
If the CLI path is ready, add an end-to-end test using: fake chunk and output modules.
```sh
notarius run dnd-session --input ./transcript.json --only spells
```
The test should use fake LLM wiring, fixture input, and a named pipeline profile
with a `spells` artifact lane.
## Done Criteria ## Done Criteria
@@ -123,8 +105,8 @@ with a `spells` artifact lane.
- D&D concepts are contained in extract module/artifact packages and docs. - D&D concepts are contained in extract module/artifact packages and docs.
- The spells module can be selected as a named artifact lane in pipeline - The spells module can be selected as a named artifact lane in pipeline
configuration. configuration.
- The first meaningful vertical slice is available through tests, and through - The first meaningful vertical slice is available through tests.
CLI if the CLI path is ready. - No CLI `run` behavior is documented or implemented until the CLI path exists.
## Review Questions ## Review Questions

View File

@@ -1,15 +1,14 @@
# Implementation Plan: Checkpoint 5 Seriatim Input Module # Implementation Plan: Checkpoint 6 D&D Spells Extractor
## Status ## Status
This is a staged implementation plan for This is a staged implementation plan for
[`5-seriatim-input-module.md`](5-seriatim-input-module.md). It is intended for [`6-dnd-spells-extractor.md`](6-dnd-spells-extractor.md). It is intended for an
an LLM coding agent to follow stage by stage. LLM coding agent to follow stage by stage.
This plan implements only checkpoint 5. Do not add D&D extraction, real domain This plan implements only checkpoint 6. Do not add D&D item, NPC, combat, or
prompts or schemas, LLM extraction calls, a `notarius run` command, broad encounter extraction; do not add broad D&D rules validation; do not add a
Seriatim schema support, or transcript-specific behavior in core framework `notarius run` command; and do not make core framework packages D&D-specific.
packages in this checkpoint.
## Policy Context ## Policy Context
@@ -20,404 +19,564 @@ Follow:
Required boundaries: Required boundaries:
- keep Seriatim JSON schema details inside `internal/modules/input/seriatim`; - keep D&D-specific artifact semantics, prompt data shaping, and response
- keep core source, runner, pipeline, extractor, validator, LLM, and config interpretation inside `internal/modules/extract/dnd/spells`;
- keep source-format details inside input modules and do not depend on concrete
Seriatim package types from the spells extractor;
- keep core source, runner, pipeline, LLM, prompt, config, and validator
packages source-agnostic and domain-agnostic; packages source-agnostic and domain-agnostic;
- do not add transcript-specific typed fields to `SourceDocument`, - use embedded prompt and schema assets instead of inline prompt/schema strings;
`SourceUnit`, runner contracts, or pipeline contracts; - register the extractor through the existing extractor registry;
- preserve transcript-specific values only as source metadata conventions;
- register the input module through the existing input adapter registry instead
of adding ad hoc conditionals;
- use flat capability strings in module metadata; - use flat capability strings in module metadata;
- keep future or planned behavior in `docs/roadmap/` until implemented. - keep future or planned behavior in `docs/roadmap/` until implemented.
## Global Implementation Decisions ## Global Implementation Decisions
- Add no new third-party dependency. Use `encoding/json` with - Add no new third-party dependency.
`Decoder.UseNumber` for Seriatim JSON parsing. - Use `dnd/spells` as the stable extractor module key.
- Use `seriatim` as the stable input adapter key. - Put concrete extractor code under `internal/modules/extract/dnd/spells`.
- Put all concrete Seriatim input code under - Use `dnd.spell_cast` as the artifact type and `v1` as the schema version.
`internal/modules/input/seriatim`. - Use `dnd.spell_casts` as the extractor-provided capability.
- Expose a small module API: - The extractor module spec must require `chunks` and `source.transcript`.
It must not require `transcript.speaker` or `transcript.timestamps`; those
metadata values may be included in prompts when present, but extraction must
not depend on Seriatim-specific helper APIs.
- Keep durable artifact payloads focused on spell data:
```go ```go
const Key = "seriatim" type SpellCast struct {
Caster string `json:"caster"`
func New() *Adapter Spell string `json:"spell"`
func ModuleSpec() pipeline.ModuleSpec Effect string `json:"effect"`
func Register(registry *pipeline.InputAdapterRegistry) error NarrativeDescription string `json:"narrative_description"`
}
``` ```
- `ModuleSpec()` must return stage `pipeline.StageInput`, no required - The LLM response model must include source references so the extractor can
capabilities, and these provided capabilities: populate `artifacts.ArtifactCandidate.SourceRefs`, but source references must
`source.transcript`, `transcript.speaker`, and `transcript.timestamps`. live in the generic artifact envelope for durable pipeline output rather than
- The Seriatim adapter should satisfy `contracts.InputAdapter`. being duplicated inside the `SpellCast` payload.
- Use `source.SourceDocument.Kind = "transcript"`. - `caster` means the in-world character or creature casting the spell. Do not
- Use `source.SourceDocument.Format = use table speaker/player as the durable caster field. Source-unit `speaker`
"application/vnd.seriatim.minimal+json"`. metadata may be rendered as optional prompt context when present.
- Use `source.SourceUnit.Kind = "transcript_segment"`. - Use `source.SourceRef` JSON field names exactly as the core type defines
- Compute `SourceDocument.Digest` from the exact raw input bytes as them: `source_id`, `start_unit_id`, and `end_unit_id`.
`sha256:<hex>`. - Trim text fields before marshaling candidate payloads. Do not silently trim
- Resolve `SourceDocument.ID` in this order: source-reference IDs; invalid source refs must be rejected by validation.
1. trimmed `contracts.ParseRequest.SourceID`, if non-empty; - `Extractor.Validators()` must return module-owned deterministic validators by
2. trimmed string `metadata.id`, if present and non-empty; default. Do not require pipeline profiles to configure validators explicitly
3. trimmed string `metadata.source_id`, if present and non-empty; for the default checkpoint behavior.
4. deterministic fallback `seriatim:<first-16-hex-chars-of-raw-sha256>`. - Treat an empty `spell_casts` response array as a successful empty extraction.
- Segment IDs become source unit IDs exactly after validation. Reject segment Treat a missing or null `spell_casts` field as malformed structured output.
IDs with leading or trailing whitespace rather than silently rewriting them. - Use synthetic D&D transcript fixture text only. Do not include private
- Copy top-level Seriatim `metadata` into `SourceDocument.Metadata`. campaign transcript content.
- Store segment `speaker`, `start`, and `end` in `SourceUnit.Metadata` under
keys with those exact names.
- Store `start` and `end` as `json.Number` values so JSON serialization remains
numeric and the original decimal representation is preserved.
- Require top-level `metadata` to be present and be an object, but do not
require any specific metadata keys in checkpoint 5.
- Require top-level `segments` to be present and contain at least one segment.
- Reject unknown or extra JSON fields only if they prevent parsing the minimal
shape. Otherwise ignore them so the module can tolerate compatible Seriatim
additions.
- Return module-specific errors prefixed with useful Seriatim context, for
example `seriatim input: segment "s1" text must not be empty`.
- Keep examples free of private transcript content. Use synthetic fixture text.
## Stage 1: Seriatim Package Skeleton And External Model ## Stage 1: Domain Model, Module Skeleton, And Registry Metadata
### Goal ### Goal
Create the Seriatim input module package, define the module-local JSON model, Create the D&D spells extractor package, define the artifact and response
and add registry-facing module metadata without changing framework contracts. models, and make the extractor discoverable through the existing extractor
registry without calling an LLM yet.
### Files To Add ### Files To Add Or Update
- `internal/modules/input/seriatim/adapter.go` - `internal/modules/extract/dnd/spells/extractor.go`
- `internal/modules/input/seriatim/model.go` - `internal/modules/extract/dnd/spells/model.go`
- `internal/modules/input/seriatim/metadata.go` - `internal/modules/extract/dnd/spells/registry_test.go`
- `internal/modules/input/seriatim/registry_test.go`
### Required API ### Required API
Add: Add:
```go ```go
package seriatim package spells
const Key = "seriatim" const Key = "dnd/spells"
const ArtifactType = "dnd.spell_cast"
const SchemaVersion = "v1"
const ( type SpellCast struct {
DocumentKind = "transcript" Caster string `json:"caster"`
UnitKind = "transcript_segment" Spell string `json:"spell"`
Format = "application/vnd.seriatim.minimal+json" Effect string `json:"effect"`
) NarrativeDescription string `json:"narrative_description"`
}
const ( type Extractor struct{}
MetadataSpeaker = "speaker"
MetadataStart = "start"
MetadataEnd = "end"
)
type Adapter struct{} func New() *Extractor
func (e *Extractor) Key() string
func New() *Adapter func (e *Extractor) ArtifactType() string
func (a *Adapter) Key() string func (e *Extractor) SchemaVersion() string
func (a *Adapter) Parse(ctx context.Context, req contracts.ParseRequest) (*source.SourceDocument, error) func (e *Extractor) Validators() []contracts.Validator
func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error)
func ModuleSpec() pipeline.ModuleSpec func ModuleSpec() pipeline.ModuleSpec
func Register(registry *pipeline.InputAdapterRegistry) error func Register(registry *pipeline.ExtractorRegistry) error
``` ```
Add typed metadata helpers: Add unexported response structs in the same package:
```go ```go
func Speaker(unit source.SourceUnit) (string, bool) type extractionResponse struct {
func Start(unit source.SourceUnit) (json.Number, bool) SpellCasts []spellCastResponse `json:"spell_casts"`
func End(unit source.SourceUnit) (json.Number, bool)
```
### Seriatim JSON Shape
Define module-local structs for the minimal external shape:
```go
type transcript struct {
Metadata map[string]any `json:"metadata"`
Segments []segment `json:"segments"`
} }
type segment struct { type spellCastResponse struct {
ID string `json:"id"` Caster string `json:"caster"`
Start json.Number `json:"start"` Spell string `json:"spell"`
End json.Number `json:"end"` Effect string `json:"effect"`
Speaker string `json:"speaker"` NarrativeDescription string `json:"narrative_description"`
Text string `json:"text"` SourceRefs []source.SourceRef `json:"source_refs"`
} }
``` ```
Use an internal decode helper based on `json.NewDecoder(bytes.NewReader(raw))`
and `UseNumber`.
### Required Behavior ### Required Behavior
- `New()` returns a non-nil adapter. - `New()` returns a non-nil extractor.
- `Adapter.Key()` returns `Key`. - `ModuleSpec()` returns defensive slices with stage `pipeline.StageExtract`,
- `ModuleSpec()` returns defensive slices and the capability set listed in the key `dnd/spells`, requires `chunks` and `source.transcript`, and provides
global decisions. `dnd.spell_casts`.
- `Register()` calls `InputAdapterRegistry.RegisterWithSpec(ModuleSpec(), ...)`. - `Register()` calls `ExtractorRegistry.RegisterWithSpec(ModuleSpec(), ...)`.
- `Register(nil)` returns an error from the registry path rather than panicking. - `Register(nil)` returns an error from the registry path rather than panicking.
- Keep external JSON structs unexported. - `Extract()` may return a clear not-yet-implemented error in this stage only.
- `Validators()` returns nil in this stage only; later stages must replace it
with the real validator chain.
### Required Tests ### Required Tests
- `New()` returns an adapter whose key is `seriatim`. - `New()` returns an extractor whose key, artifact type, and schema version
- `ModuleSpec()` uses input stage and declares the required provided match the constants.
capabilities. - `ModuleSpec()` uses extract stage and declares the required capabilities.
- `Register()` makes the adapter buildable from an `InputAdapterRegistry`. - Caller mutation of `ModuleSpec().Provides` or `ModuleSpec().Requires` does not
- Registry lookup returns the Seriatim module spec. affect later calls.
- `Register()` makes the extractor buildable from an `ExtractorRegistry`.
- Registry lookup returns the spells module spec.
- `Register(nil)` returns an error containing extractor registry context.
### Validation ### Validation
Run: Run:
```sh ```sh
gofmt -w internal/modules/input/seriatim gofmt -w internal/modules/extract/dnd/spells
go test ./internal/modules/input/seriatim go test ./internal/modules/extract/dnd/spells
go test ./... go test ./...
``` ```
## Stage 2: Parse, Validate, And Map To SourceDocument ## Stage 2: Structured Response Schema Asset
### Goal ### Goal
Implement the Seriatim parser and mapper from minimal Seriatim JSON into the Add the structured response schema used by the D&D spells extractor and register
generic source model. it through the existing LLM schema registry.
### Files To Add Or Update ### Files To Add Or Update
- `internal/modules/input/seriatim/adapter.go` - `internal/framework/llm/assets/schemas/dnd_spells.v1.json`
- `internal/modules/input/seriatim/model.go` - `internal/framework/llm/schema_registry.go`
- `internal/modules/input/seriatim/adapter_test.go` - `internal/framework/llm/schema_registry_test.go`
- `internal/modules/input/seriatim/testdata/valid_minimal.json` - `internal/modules/extract/dnd/spells/schema_test.go`
- `internal/modules/input/seriatim/testdata/duplicate_segment_id.json`
### Required Validation ### Schema Decisions
Reject: Register:
- nil or canceled context before parsing; - response schema key: `dnd_spells`
- empty raw input; - schema ID: `notarius.dnd.spells`
- malformed JSON; - schema version: `v1`
- valid JSON with trailing non-whitespace data; - response schema name: `notarius_dnd_spells_v1`
- missing, null, or non-object top-level `metadata`; - asset path: `assets/schemas/dnd_spells.v1.json`
- missing, null, empty, or non-array top-level `segments`;
- segment IDs that are empty after trimming;
- segment IDs with leading or trailing whitespace;
- duplicate segment IDs;
- missing or empty `speaker`;
- missing, empty, non-numeric, negative, or non-finite `start`;
- missing, empty, non-numeric, negative, or non-finite `end`;
- segments where `end < start`;
- missing or empty `text`.
The parser may preserve leading and trailing whitespace in segment text as long The JSON schema must require a top-level object:
as the text is not empty after trimming.
### Mapping Rules ```json
{
"spell_casts": [
{
"caster": "Aria",
"spell": "Cure Wounds",
"effect": "Heals an injured ally.",
"narrative_description": "Aria casts Cure Wounds after the fight.",
"source_refs": [
{
"source_id": "session-alpha",
"start_unit_id": "seg-001",
"end_unit_id": "seg-002"
}
]
}
]
}
```
- `segment.id` becomes `SourceUnit.ID`. Required schema constraints:
- `segment.text` becomes `SourceUnit.Text`.
- `speaker`, `start`, and `end` become unit metadata under the exact keys - `additionalProperties: false` at every object level.
defined in stage 1. - top-level `spell_casts` is required and must be an array.
- The document metadata is a shallow copy of top-level Seriatim metadata. - `spell_casts` may be empty.
- The document digest is based on raw input bytes, not normalized JSON. - each spell cast requires non-empty `caster`, `spell`, `effect`, and
- Call `source.ValidateDocument` before returning the document and wrap any `narrative_description`.
validation failure with Seriatim context. - each spell cast requires `source_refs` with `minItems: 1`.
- each source ref requires non-empty `source_id`, `start_unit_id`, and
`end_unit_id`.
### Required Tests ### Required Tests
Add tests for: - `llm.LookupResponseSchema(llm.DNDSpellsSchemaKey)` succeeds.
- Registered schema list remains sorted and now includes the D&D spells schema.
- valid minimal transcript parses to a source document with expected ID, kind, - The schema content is valid JSON and mutation-safe through registry lookups.
format, digest, units, and metadata; - The spells package can look up the schema key it will use during extraction.
- `ParseRequest.SourceID` overrides metadata-derived IDs; - The schema diagnostics map omits raw schema content.
- fallback document ID is deterministic and has prefix `seriatim:`;
- malformed JSON returns an actionable Seriatim parse error;
- missing metadata is rejected;
- missing or empty segments is rejected;
- duplicate segment IDs are rejected;
- empty segment text is rejected;
- missing speaker is rejected;
- invalid timestamp values are rejected;
- `end < start` is rejected;
- typed metadata helpers return the expected speaker and timestamp values;
- a `source.SourceRef` using the first and last generated unit IDs validates
with `source.ValidateRef`.
### Validation ### Validation
Run: Run:
```sh ```sh
gofmt -w internal/modules/input/seriatim gofmt -w internal/framework/llm internal/modules/extract/dnd/spells
go test ./internal/modules/input/seriatim go test ./internal/framework/llm
go test ./internal/modules/extract/dnd/spells
go test ./... go test ./...
``` ```
## Stage 3: Pipeline Resolution And Config Compatibility ## Stage 3: Prompt Assets And Prompt Rendering
### Goal ### Goal
Prove the Seriatim input module participates in pipeline-profile resolution and Add embedded prompt assets for D&D spell extraction and render deterministic
capability validation through existing registries and config loading. system/user messages from generic source chunks.
### Files To Add Or Update ### Files To Add Or Update
- `internal/modules/input/seriatim/config_test.go` - `internal/framework/prompt/assets/dnd/spells/system.md`
- `internal/modules/input/seriatim/testdata/pipeline.yml` - `internal/framework/prompt/assets/dnd/spells/user.md`
- `internal/framework/prompt/registry.go`
- `internal/framework/prompt/registry_test.go`
- `internal/framework/prompt/render_test.go`
- `internal/modules/extract/dnd/spells/prompt.go`
- `internal/modules/extract/dnd/spells/prompt_test.go`
### Prompt Decisions
Register:
- prompt ID: `dnd.spells`
- prompt constant: `DNDSpellsPromptID`
- prompt version: `v1`
- embedded directory: `assets/dnd/spells`
The system prompt must:
- include the shared prompt hardening text via `{{ hardening }}`;
- identify the task as extracting D&D spell casts only;
- state that the model must not infer spells not supported by the provided
source text;
- state that source references must use source-unit IDs exactly as provided.
The user prompt must render:
- source document ID;
- chunk ID and chunk index when a chunk is present;
- source units in their existing order;
- each source unit's ID and text;
- selected metadata only as optional context, using generic labels. Include
`speaker`, `start`, and `end` when present without importing Seriatim helper
APIs.
Add a module-local prompt data builder that accepts
`contracts.ExtractionRequest` and returns template data. It must not mutate the
request, source document, chunk, units, or metadata maps.
### Required Tests
- Prompt metadata lookup succeeds for `dnd.spells`.
- Registered prompt metadata remains sorted.
- Rendering includes hardening text.
- Rendering includes source ID, chunk ID, unit IDs, unit text, and optional
speaker/timestamp metadata when present.
- Rendering fails clearly if required template data is missing.
- Module-local prompt data construction works with a generic source chunk and
does not depend on concrete Seriatim package helpers.
### Validation
Run:
```sh
gofmt -w internal/framework/prompt internal/modules/extract/dnd/spells
go test ./internal/framework/prompt
go test ./internal/modules/extract/dnd/spells
go test ./...
```
## Stage 4: Extractor LLM Call And Candidate Mapping
### Goal
Implement `Extractor.Extract` so a source chunk can flow through prompt
rendering, structured LLM completion, and candidate creation.
### Files To Add Or Update
- `internal/modules/extract/dnd/spells/extractor.go`
- `internal/modules/extract/dnd/spells/model.go`
- `internal/modules/extract/dnd/spells/extractor_test.go`
### Required Behavior
`Extract()` must:
- reject nil extractor, nil context, canceled context, nil source, nil chunk,
empty chunk units, and nil LLM client with clear `dnd spells` error context;
- render the `dnd.spells` prompt using the stage 3 prompt data builder;
- look up the stage 2 response schema;
- call `req.LLMClient.CompleteStructured` with:
- `StageName: Key`;
- two messages, system then user;
- `ResponseSchemaName: schema.Name`;
- `ResponseSchema: schema.JSONSchema`;
- decode into `extractionResponse`;
- reject a nil `SpellCasts` slice as malformed structured output;
- return no candidates for an empty `SpellCasts` slice;
- preserve response order when creating candidates;
- trim `caster`, `spell`, `effect`, and `narrative_description` before
marshaling the `SpellCast` payload;
- copy response source refs into `ArtifactCandidate.SourceRefs`;
- leave `ExtractorKey`, `ArtifactType`, `SchemaVersion`, and `Index` empty or
zero so the runner's existing candidate normalization remains authoritative;
- wrap LLM, prompt, schema lookup, response validation, and JSON marshal errors
with useful `dnd spells` context.
Do not call validators from `Extract()`. Validation belongs to the runner's
validator phase.
### Required Tests
Use a fake `contracts.StructuredLLMClient`.
- Successful extraction returns one candidate with expected payload fields,
source refs, response schema name, schema JSON, prompt messages, and stage
name.
- Empty `spell_casts: []` returns no candidates and no error.
- Missing or null `spell_casts` is rejected as malformed structured output.
- LLM client errors are wrapped with D&D spells context.
- Nil source, nil chunk, empty chunk units, nil LLM client, nil context, and
canceled context are rejected.
- Multiple response spell casts produce candidates in response order.
- Candidate source refs are copied so later mutation of the fake response does
not mutate returned candidates.
### Validation
Run:
```sh
gofmt -w internal/modules/extract/dnd/spells
go test ./internal/modules/extract/dnd/spells
go test ./...
```
## Stage 5: Deterministic Validator Chain
### Goal
Add module-owned deterministic validators for spell payload shape, required
fields, and source-reference grounding.
### Files To Add Or Update
- `internal/modules/extract/dnd/spells/validator.go`
- `internal/modules/extract/dnd/spells/validator_test.go`
- `internal/modules/extract/dnd/spells/extractor.go`
### Validator Decisions
Add two validators:
1. `ShapeValidator`
- `Name()` returns `dnd/spells/shape`.
- Rejects malformed JSON payloads with reason code `invalid_payload`.
- Rejects blank `caster`, `spell`, `effect`, or `narrative_description`
with reason code `missing_required_field`.
- Approves candidates with valid payload shape and required fields.
2. `SourceRefValidator`
- `Name()` returns `dnd/spells/source_refs`.
- Rejects candidates with no source refs using reason code
`missing_source_ref`.
- Rejects any invalid source ref using reason code `invalid_source_ref` and
the `source.ValidateRef` error message.
- Approves candidates whose source refs all validate against the request
source document.
`Extractor.Validators()` must return `ShapeValidator{}` followed by
`SourceRefValidator{}`. Return a fresh slice each time.
Both validators must:
- satisfy `contracts.Validator`;
- return one decision for every candidate;
- preserve candidate indexes in decisions;
- return an error, not rejection decisions, when called with nil source only if
source-reference validation cannot run. Shape validation does not require a
source document.
### Required Tests
- `Extractor.Validators()` returns the two validators in the required order and
is mutation-safe.
- Each validator approves a valid candidate.
- Shape validator rejects malformed payload JSON.
- Shape validator rejects each blank required field.
- Source ref validator rejects missing refs.
- Source ref validator rejects unknown source IDs, unknown unit IDs, and
reversed unit ranges.
- Both validators return one decision per candidate and preserve indexes.
- Approved decisions use `validate.Approved`; rejected decisions use the reason
codes listed above.
### Validation
Run:
```sh
gofmt -w internal/modules/extract/dnd/spells
go test ./internal/modules/extract/dnd/spells
go test ./...
```
## Stage 6: Pipeline Resolution And Runner Integration
### Goal
Prove Seriatim input can flow through the existing runner into the real D&D
spells extractor, fake downstream infrastructure, and the extractor-owned
validators.
### Files To Add Or Update
- `internal/modules/extract/dnd/spells/config_test.go`
- `internal/modules/extract/dnd/spells/runner_test.go`
- `internal/modules/extract/dnd/spells/testdata/seriatim_spell_session.json`
- `internal/modules/extract/dnd/spells/testdata/pipeline.yml`
### Required Test Catalog ### Required Test Catalog
Build a test-only module catalog with: Build test-only catalogs and registries with:
- Seriatim input registered through `seriatim.Register`; - Seriatim input registered through `seriatim.Register`;
- a fake chunker requiring `source.transcript` and providing `chunks`; - a fake chunker registered as `fake/chunk`, requiring `source.transcript` and
- a fake extractor requiring `chunks`, `transcript.speaker`, and providing `chunks`;
`transcript.timestamps`, and providing `fake.artifacts`; - D&D spells extractor registered through `spells.Register`;
- `pipeline.AppendOrderMerger` registered as `appendorder`, requiring - `pipeline.AppendOrderMerger` registered as `appendorder`, requiring
`fake.artifacts`; `dnd.spell_casts`;
- `pipeline.NoopNormalizer` registered as `noop`; - `pipeline.NoopNormalizer` registered as `noop`;
- a fake `json` output encoder registered as output stage. - a fake `json` output encoder registered as output stage.
Do not add real extract, chunk, normalize, or output modules for this checkpoint. Do not add real chunk or output modules for this checkpoint.
### YAML Fixture ### YAML Fixture
Use a synthetic pipeline fixture shaped like: Use a synthetic pipeline fixture:
```yaml ```yaml
version: 1 version: 1
pipelines: pipelines:
seriatim-fixture: dnd-spells-fixture:
input: seriatim input: seriatim
chunk: fake/chunk chunk: fake/chunk
artifacts: artifacts:
events: spells:
extract: fake/extract extract: dnd/spells
merge: appendorder merge: appendorder
normalize: noop normalize: noop
output: json output: json
``` ```
The default LLM profile supplied by `config.Default()` is sufficient. Do not
add real provider settings to this fixture.
### Required Tests ### Required Tests
- `config.ParseFileConfigYAML` and `Config.ApplyFileConfig` load the fixture. - `config.ParseFileConfigYAML` and `Config.ApplyFileConfig` load the fixture.
- `Config.Resolve` succeeds with pipeline ID `seriatim-fixture` and the - `Config.Resolve` succeeds with pipeline ID `dnd-spells-fixture` and the
test-only catalog. test-only catalog.
- The resolved pipeline input module is `seriatim`. - The resolved artifact lane ID is `spells` and extractor module is
`dnd/spells`.
- The resolved pipeline digest is non-empty and stable across repeated - The resolved pipeline digest is non-empty and stable across repeated
resolution. resolution.
- Removing `transcript.timestamps` from the Seriatim module spec in the - Removing `source.transcript` from the Seriatim module spec causes resolution
test-only catalog causes resolution to fail with a missing capability error. to fail with a missing capability error for `dnd/spells`.
- Removing `dnd.spell_casts` from the extractor spec causes resolution to fail
with a missing capability error for `appendorder`.
- Selecting an unknown `--only` lane still fails through existing resolution - Selecting an unknown `--only` lane still fails through existing resolution
behavior. behavior.
- Runner success path:
- parse the Seriatim fixture;
- use a fake LLM that returns at least two valid spell casts;
- assert approved artifacts preserve response order;
- assert approved payloads contain spell data;
- assert approved artifact source refs validate with `source.ValidateRef`;
- assert the manifest records input module `seriatim`, extractor
`dnd/spells`, lane `spells`, and validation status `approved`.
- Runner rejection path:
- fake LLM returns a spell cast with an invalid source ref;
- runner completes with rejected artifacts and validation status `rejected`;
- rejection contains validator `dnd/spells/source_refs` and reason code
`invalid_source_ref`.
- Runner malformed-output path:
- fake LLM returns or reports malformed structured output;
- runner returns an extraction error with D&D spells context and failed
validation status.
### Validation ### Validation
Run: Run:
```sh ```sh
gofmt -w internal/modules/input/seriatim gofmt -w internal/modules/extract/dnd/spells
go test ./internal/modules/input/seriatim go test ./internal/modules/extract/dnd/spells
go test ./internal/core/config
go test ./...
```
## Stage 4: Runner Integration With Fake Downstream Stages
### Goal
Prove real Seriatim input can flow through the existing runner into fake
downstream stages while preserving source-unit IDs and metadata.
### Files To Add Or Update
- `internal/modules/input/seriatim/runner_test.go`
### Required Behavior
Use the same Seriatim fixture from stage 2 and a resolved pipeline from stage 3.
Register fake downstream stages only inside the test.
The fake extractor should:
- inspect the received `SourceDocument` and `SourceChunk`;
- assert that unit IDs match Seriatim segment IDs;
- assert that speaker and timestamp metadata are present;
- return one generic artifact candidate with a source reference pointing at
existing Seriatim-derived unit IDs.
The test should then assert:
- `Runner.Run` succeeds;
- the manifest records input module `seriatim`;
- the manifest source digest equals the parsed document digest;
- approved artifacts preserve valid source references;
- no transcript-specific type has been added outside the module.
### Required Tests
- successful runner execution from Seriatim JSON through fake chunk, extract,
merge, normalize, and output stages;
- runner failure when the Seriatim adapter returns an invalid source document,
using a malformed fixture or test input;
- validation of the fake candidate's source reference with
`source.ValidateRef`.
### Validation
Run:
```sh
gofmt -w internal/modules/input/seriatim
go test ./internal/modules/input/seriatim go test ./internal/modules/input/seriatim
go test ./internal/framework/pipeline go test ./internal/framework/pipeline
go test ./... go test ./...
``` ```
## Stage 5: Documentation And Final Verification ## Stage 7: Documentation And Final Verification
### Goal ### Goal
Document the implemented Seriatim integration contract once the module exists, Document the implemented D&D spells extraction contract without describing
without describing unimplemented D&D extraction or run-command behavior. unimplemented extractors or a `notarius run` command.
### Files To Add Or Update ### Files To Add Or Update
- `docs/integrations/seriatim.md` - `docs/integrations/dnd-spells.md`
- `docs/roadmap/5-seriatim-input-module.md` - `docs/roadmap/6-dnd-spells-extractor.md`
### Required Documentation ### Required Documentation
Create `docs/integrations/seriatim.md` as implemented-behavior documentation Create `docs/integrations/dnd-spells.md` as implemented-behavior documentation
with: with:
- accepted minimal JSON shape; - module key, artifact type, schema version, prompt ID, and response schema key;
- required fields and validation rules; - accepted source expectations: generic source document/chunk with transcript
- mapping from Seriatim fields to `SourceDocument` and `SourceUnit`; capability supplied by pipeline resolution;
- metadata key conventions for `speaker`, `start`, and `end`; - spell payload fields;
- capability strings declared by the module; - source-reference behavior: LLM response includes refs, durable artifact
- note that broader Seriatim schema variants are not yet supported. output carries refs in the generic artifact envelope;
- default deterministic validators and rejection reason codes;
- declared required/provided capabilities;
- note that item, NPC, combat, encounter, broad D&D rules, and CLI `run`
workflows are not implemented by this checkpoint.
Update `docs/roadmap/5-seriatim-input-module.md` only if implementation Update `docs/roadmap/6-dnd-spells-extractor.md` only if implementation reveals
reveals a real scope or policy correction. Keep future D&D extraction behavior a real scope or policy correction. Keep implementation staging in this file,
out of the integration doc. not in the feature roadmap.
### Final Validation ### Final Validation
Run: Run:
```sh ```sh
gofmt -w internal/modules/input/seriatim gofmt -w internal/modules/extract/dnd/spells internal/framework/llm internal/framework/prompt
go test ./... go test ./...
go build ./cmd/notarius go build ./cmd/notarius
rm -f ./notarius rm -f ./notarius
@@ -427,17 +586,19 @@ rm -f ./notarius
- `go test ./...` passes. - `go test ./...` passes.
- `go build ./cmd/notarius` passes. - `go build ./cmd/notarius` passes.
- Seriatim minimal transcript JSON maps into `SourceDocument`. - The D&D spells extractor is registered through the extractor registry.
- Unit IDs are stable and validate in source references. - Pipeline-profile resolution can select a `spells` artifact lane using
- Transcript fields do not appear in core runner contracts. extractor module `dnd/spells`.
- The input module is selectable through the input registry and pipeline-profile - Seriatim minimal transcript JSON can flow through the runner into the D&D
resolution. spells extractor in tests.
- The input module declares transcript-oriented flat capabilities for pipeline - Spell artifacts include valid generic source references in the artifact
validation. envelope.
- Tests prove transcript-specific assumptions are isolated to - Invalid source references are rejected by the extractor-owned validator chain.
`internal/modules/input/seriatim`. - D&D concepts do not appear in core runner, source, pipeline, config, prompt,
LLM, or validator contracts.
- No CLI `run` behavior is documented or implemented in this checkpoint.
## Open Questions ## Open Questions
None. This plan chooses the checkpoint-5 behavior needed to implement the None. This plan chooses the checkpoint-6 behavior needed to implement the
feature without requiring additional product decisions. feature without requiring additional product decisions.