20 KiB
Implementation Plan: Checkpoint 6 D&D Spells Extractor
Status
This is a staged implementation plan for
6-dnd-spells-extractor.md. It is intended for an
LLM coding agent to follow stage by stage.
This plan implements only checkpoint 6. Do not add D&D item, NPC, combat, or
encounter extraction; do not add broad D&D rules validation; do not add a
notarius run command; and do not make core framework packages D&D-specific.
Policy Context
Follow:
Required boundaries:
- keep D&D-specific artifact semantics, prompt data shaping, and response
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;
- use embedded prompt and schema assets instead of inline prompt/schema strings;
- register the extractor through the existing extractor registry;
- use flat capability strings in module metadata;
- keep future or planned behavior in
docs/roadmap/until implemented.
Global Implementation Decisions
- Add no new third-party dependency.
- Use
dnd/spellsas the stable extractor module key. - Put concrete extractor code under
internal/modules/extract/dnd/spells. - Use
dnd.spell_castas the artifact type andv1as the schema version. - Use
dnd.spell_castsas the extractor-provided capability. - The extractor module spec must require
chunksandsource.transcript. It must not requiretranscript.speakerortranscript.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:
type SpellCast struct {
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
}
- The LLM response model must include source references so the extractor can
populate
artifacts.ArtifactCandidate.SourceRefs, but source references must live in the generic artifact envelope for durable pipeline output rather than being duplicated inside theSpellCastpayload. castermeans the in-world character or creature casting the spell. Do not use table speaker/player as the durable caster field. Source-unitspeakermetadata may be rendered as optional prompt context when present.- Use
source.SourceRefJSON field names exactly as the core type defines them:source_id,start_unit_id, andend_unit_id. - Trim text fields before marshaling candidate payloads. Do not silently trim source-reference IDs; invalid source refs must be rejected by validation.
Extractor.Validators()must return module-owned deterministic validators by default. Do not require pipeline profiles to configure validators explicitly for the default checkpoint behavior.- Treat an empty
spell_castsresponse array as a successful empty extraction. Treat a missing or nullspell_castsfield as malformed structured output. - Use synthetic D&D transcript fixture text only. Do not include private campaign transcript content.
Stage 1: Domain Model, Module Skeleton, And Registry Metadata
Goal
Create the D&D spells extractor package, define the artifact and response models, and make the extractor discoverable through the existing extractor registry without calling an LLM yet.
Files To Add Or Update
internal/modules/extract/dnd/spells/extractor.gointernal/modules/extract/dnd/spells/model.gointernal/modules/extract/dnd/spells/registry_test.go
Required API
Add:
package spells
const Key = "dnd/spells"
const ArtifactType = "dnd.spell_cast"
const SchemaVersion = "v1"
type SpellCast struct {
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
}
type Extractor struct{}
func New() *Extractor
func (e *Extractor) Key() string
func (e *Extractor) ArtifactType() string
func (e *Extractor) SchemaVersion() string
func (e *Extractor) Validators() []contracts.Validator
func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error)
func ModuleSpec() pipeline.ModuleSpec
func Register(registry *pipeline.ExtractorRegistry) error
Add unexported response structs in the same package:
type extractionResponse struct {
SpellCasts []spellCastResponse `json:"spell_casts"`
}
type spellCastResponse struct {
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
SourceRefs []source.SourceRef `json:"source_refs"`
}
Required Behavior
New()returns a non-nil extractor.ModuleSpec()returns defensive slices with stagepipeline.StageExtract, keydnd/spells, requireschunksandsource.transcript, and providesdnd.spell_casts.Register()callsExtractorRegistry.RegisterWithSpec(ModuleSpec(), ...).Register(nil)returns an error from the registry path rather than panicking.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
New()returns an extractor whose key, artifact type, and schema version match the constants.ModuleSpec()uses extract stage and declares the required capabilities.- Caller mutation of
ModuleSpec().ProvidesorModuleSpec().Requiresdoes not affect later calls. Register()makes the extractor buildable from anExtractorRegistry.- Registry lookup returns the spells module spec.
Register(nil)returns an error containing extractor registry context.
Validation
Run:
gofmt -w internal/modules/extract/dnd/spells
go test ./internal/modules/extract/dnd/spells
go test ./...
Stage 2: Structured Response Schema Asset
Goal
Add the structured response schema used by the D&D spells extractor and register it through the existing LLM schema registry.
Files To Add Or Update
internal/framework/llm/assets/schemas/dnd_spells.v1.jsoninternal/framework/llm/schema_registry.gointernal/framework/llm/schema_registry_test.gointernal/modules/extract/dnd/spells/schema_test.go
Schema Decisions
Register:
- response schema key:
dnd_spells - schema ID:
notarius.dnd.spells - schema version:
v1 - response schema name:
notarius_dnd_spells_v1 - asset path:
assets/schemas/dnd_spells.v1.json
The JSON schema must require a top-level object:
{
"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"
}
]
}
]
}
Required schema constraints:
additionalProperties: falseat every object level.- top-level
spell_castsis required and must be an array. spell_castsmay be empty.- each spell cast requires non-empty
caster,spell,effect, andnarrative_description. - each spell cast requires
source_refswithminItems: 1. - each source ref requires non-empty
source_id,start_unit_id, andend_unit_id.
Required Tests
llm.LookupResponseSchema(llm.DNDSpellsSchemaKey)succeeds.- Registered schema list remains sorted and now includes the D&D spells schema.
- The schema content is valid JSON and mutation-safe through registry lookups.
- The spells package can look up the schema key it will use during extraction.
- The schema diagnostics map omits raw schema content.
Validation
Run:
gofmt -w internal/framework/llm internal/modules/extract/dnd/spells
go test ./internal/framework/llm
go test ./internal/modules/extract/dnd/spells
go test ./...
Stage 3: Prompt Assets And Prompt Rendering
Goal
Add embedded prompt assets for D&D spell extraction and render deterministic system/user messages from generic source chunks.
Files To Add Or Update
internal/framework/prompt/assets/dnd/spells/system.mdinternal/framework/prompt/assets/dnd/spells/user.mdinternal/framework/prompt/registry.gointernal/framework/prompt/registry_test.gointernal/framework/prompt/render_test.gointernal/modules/extract/dnd/spells/prompt.gointernal/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, andendwhen 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:
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.gointernal/modules/extract/dnd/spells/model.gointernal/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 spellserror context; - render the
dnd.spellsprompt using the stage 3 prompt data builder; - look up the stage 2 response schema;
- call
req.LLMClient.CompleteStructuredwith:StageName: Key;- two messages, system then user;
ResponseSchemaName: schema.Name;ResponseSchema: schema.JSONSchema;
- decode into
extractionResponse; - reject a nil
SpellCastsslice as malformed structured output; - return no candidates for an empty
SpellCastsslice; - preserve response order when creating candidates;
- trim
caster,spell,effect, andnarrative_descriptionbefore marshaling theSpellCastpayload; - copy response source refs into
ArtifactCandidate.SourceRefs; - leave
ExtractorKey,ArtifactType,SchemaVersion, andIndexempty 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 spellscontext.
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_castsis 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:
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.gointernal/modules/extract/dnd/spells/validator_test.gointernal/modules/extract/dnd/spells/extractor.go
Validator Decisions
Add two validators:
-
ShapeValidatorName()returnsdnd/spells/shape.- Rejects malformed JSON payloads with reason code
invalid_payload. - Rejects blank
caster,spell,effect, ornarrative_descriptionwith reason codemissing_required_field. - Approves candidates with valid payload shape and required fields.
-
SourceRefValidatorName()returnsdnd/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_refand thesource.ValidateReferror 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:
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.gointernal/modules/extract/dnd/spells/runner_test.gointernal/modules/extract/dnd/spells/testdata/seriatim_spell_session.jsoninternal/modules/extract/dnd/spells/testdata/pipeline.yml
Required Test Catalog
Build test-only catalogs and registries with:
- Seriatim input registered through
seriatim.Register; - a fake chunker registered as
fake/chunk, requiringsource.transcriptand providingchunks; - D&D spells extractor registered through
spells.Register; pipeline.AppendOrderMergerregistered asappendorder, requiringdnd.spell_casts;pipeline.NoopNormalizerregistered asnoop;- a fake
jsonoutput encoder registered as output stage.
Do not add real chunk or output modules for this checkpoint.
YAML Fixture
Use a synthetic pipeline fixture:
version: 1
pipelines:
dnd-spells-fixture:
input: seriatim
chunk: fake/chunk
artifacts:
spells:
extract: dnd/spells
merge: appendorder
normalize: noop
output: json
Required Tests
config.ParseFileConfigYAMLandConfig.ApplyFileConfigload the fixture.Config.Resolvesucceeds with pipeline IDdnd-spells-fixtureand the test-only catalog.- The resolved artifact lane ID is
spellsand extractor module isdnd/spells. - The resolved pipeline digest is non-empty and stable across repeated resolution.
- Removing
source.transcriptfrom the Seriatim module spec causes resolution to fail with a missing capability error fordnd/spells. - Removing
dnd.spell_castsfrom the extractor spec causes resolution to fail with a missing capability error forappendorder. - Selecting an unknown
--onlylane still fails through existing resolution 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, extractordnd/spells, lanespells, and validation statusapproved.
- 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_refsand reason codeinvalid_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
Run:
gofmt -w internal/modules/extract/dnd/spells
go test ./internal/modules/extract/dnd/spells
go test ./internal/modules/input/seriatim
go test ./internal/framework/pipeline
go test ./...
Stage 7: Documentation And Final Verification
Goal
Document the implemented D&D spells extraction contract without describing
unimplemented extractors or a notarius run command.
Files To Add Or Update
docs/integrations/dnd-spells.mddocs/roadmap/6-dnd-spells-extractor.md
Required Documentation
Create docs/integrations/dnd-spells.md as implemented-behavior documentation
with:
- module key, artifact type, schema version, prompt ID, and response schema key;
- accepted source expectations: generic source document/chunk with transcript capability supplied by pipeline resolution;
- spell payload fields;
- source-reference behavior: LLM response includes refs, durable artifact 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
runworkflows are not implemented by this checkpoint.
Update docs/roadmap/6-dnd-spells-extractor.md only if implementation reveals
a real scope or policy correction. Keep implementation staging in this file,
not in the feature roadmap.
Final Validation
Run:
gofmt -w internal/modules/extract/dnd/spells internal/framework/llm internal/framework/prompt
go test ./...
go build ./cmd/notarius
rm -f ./notarius
Done Criteria
go test ./...passes.go build ./cmd/notariuspasses.- The D&D spells extractor is registered through the extractor registry.
- Pipeline-profile resolution can select a
spellsartifact lane using extractor modulednd/spells. - Seriatim minimal transcript JSON can flow through the runner into the D&D spells extractor in tests.
- Spell artifacts include valid generic source references in the artifact envelope.
- Invalid source references are rejected by the extractor-owned validator chain.
- D&D concepts do not appear in core runner, source, pipeline, config, prompt, LLM, or validator contracts.
- No CLI
runbehavior is documented or implemented in this checkpoint.
Open Questions
None. This plan chooses the checkpoint-6 behavior needed to implement the feature without requiring additional product decisions.