diff --git a/docs/internal/llm.md b/docs/internal/llm.md index 0c41350..b0735b1 100644 --- a/docs/internal/llm.md +++ b/docs/internal/llm.md @@ -80,7 +80,9 @@ configuration facts in [Configuration](../config.md#defaults). `AssetRegistry` combines caller-owned prompt filesystems under stable prefixes and rejects invalid or conflicting registrations. Production module packages register their own prompt and schema assets; generic framework code contains no -D&D prompt content. +D&D prompt content. `internal/framework/promptfs` provides the domain-neutral +filesystem composition helper used to combine module-owned files with shared +domain prompt fragments. Schema helpers load embedded JSON Schema with identity and digest metadata, return defensive copies, and expose a diagnostics map that omits schema bytes. diff --git a/docs/internal/modules.md b/docs/internal/modules.md index 467ce2e..79e4017 100644 --- a/docs/internal/modules.md +++ b/docs/internal/modules.md @@ -1,7 +1,7 @@ # Module And Validator Internals -Production module implementations and generic validators live under -`internal/modules`; the current D&D validators live under `internal/validators`. +Production module and validator implementations live under their domain-first +trees in `internal/modules`. The selectable keys, configuration options, reference slots, and default validator chain are canonical in the [module](../config.md#implemented-production-modules) and @@ -26,10 +26,10 @@ that agreement. Runtime delivery uses the corresponding stage request's LLM-backed extensions own their prompt definitions and response schemas under package-local embedded assets. Shared filesystem composition belongs in -`internal/modules/sharedassets`; reusable D&D prompt fragments, reference +`internal/framework/promptfs`; reusable D&D prompt fragments, reference declarations, prompt-input assembly, and source-unit helpers belong in -`internal/modules/sharedassets/dnd`. Stage contracts expose only Notarius -structured-completion types, not Scriptorium public types. +`internal/modules/dnd/shared`. Stage contracts expose only Notarius structured- +completion types, not Scriptorium public types. Reference material may inform a module or prompt but must not become source evidence. The resolver and materializer behavior is described in @@ -62,7 +62,7 @@ The accepted options and defaults are defined in [Configuration](../config.md#implemented-production-modules). Generic framework validation canonicalizes the returned unit slices before extraction. -### `internal/modules/chunk/dnd/scenes` +### `internal/modules/dnd/chunk/scenes` The scene chunker prepares a structured Scriptorium request from the full transcript, session, and optional D&D reference inputs. It validates the model's @@ -82,7 +82,7 @@ file types remain canonical in [Configuration](../config.md). ## Extractor -### `internal/modules/extract/dnd/spells` +### `internal/modules/dnd/extract/spells` The spell extractor prepares a structured request from one chunk, the chunk-scoped source input, the session, and optional D&D reference inputs. It @@ -141,7 +141,7 @@ from schema loading or compilation errors. Neither validator calls the LLM. ## D&D Spell Validators -`internal/validators/extract/dnd/spells/spellpayload` provides strict decoding, +`internal/modules/dnd/validate/spells/spellpayload` provides strict decoding, shape checks, source-reference candidates, and cited-text lookup shared by the three validators. @@ -199,5 +199,7 @@ does not inventory implementations. defaults. - `internal/cli/run_test.go`: production catalog, config resolution, and end-to-end CLI composition. -- `internal/modules/sharedassets/**/*_test.go`: shared prompt and reference - assembly. +- `internal/framework/promptfs/*_test.go` and + `internal/modules/dnd/shared/*_test.go`: shared prompt and reference assembly. +- `internal/modules/integration/*_test.go`: black-box composition across + production extension domains. diff --git a/docs/internal/overview.md b/docs/internal/overview.md index dbcfbc9..5bab987 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -42,6 +42,7 @@ a sorted set of artifact lanes before the runner constructs any stage module. | `internal/framework/pipeline` | Registries, profile resolution, capability checks, reference materialization, validator-chain resolution, retries, orchestration, warnings, and manifest population. | | `internal/framework/validate` | Shared validator decision and cardinality helpers. | | `internal/framework/llm` | Scriptorium-backed structured completions, prompt/schema registration, scheduling, profile recording, and secret redaction. | +| `internal/framework/promptfs` | Builds module prompt filesystems from module-owned and caller-provided shared prompt assets. | | `internal/framework/checkpoint` | Workspace-backed checkpoint loading, recording, and payload serialization. | | `internal/framework/debug` | Workspace-backed framework and LLM debug recording. | @@ -60,26 +61,25 @@ Configuration. The implemented module packages are: | --- | --- | | `internal/modules/seriatim/input/transcript` | Parses the supported Seriatim transcript format into the generic source model. | | `internal/modules/generic/chunk/units` | Splits ordered source units by unit count and overlap. | -| `internal/modules/chunk/dnd/scenes` | Produces contiguous D&D scene chunks from structured model output. | -| `internal/modules/extract/dnd/spells` | Produces source-grounded D&D spell-cast raw output. | +| `internal/modules/dnd/chunk/scenes` | Produces contiguous D&D scene chunks from structured model output. | +| `internal/modules/dnd/extract/spells` | Produces source-grounded D&D spell-cast raw output. | | `internal/modules/generic/merge/appendorder` | Combines accepted extraction results in chunk order. | | `internal/modules/generic/normalize/noop` | Preserves accepted merged output. | | `internal/modules/generic/output/json` | Encodes manifests, lane payloads, warnings, and rejections as logical JSON files. | -`internal/modules/sharedassets` composes shared prompt filesystems. -`internal/modules/sharedassets/dnd` owns reusable D&D prompt fragments, +`internal/modules/dnd/shared` owns reusable D&D prompt fragments, reference declarations, prompt input assembly, and source-unit reference -helpers. +helpers. Domain-neutral prompt filesystem composition lives in +`internal/framework/promptfs`. Generic validators under `internal/modules/generic/validate` provide unconditional test decisions, JSON syntax validation, and JSON Schema -validation. D&D spell validators remain under `internal/validators` and provide -shape, source-reference, and source-relatedness decisions, with `spellpayload` -holding their shared parser and lookup helpers. +validation. D&D spell validators under `internal/modules/dnd/validate/spells` +provide shape, source-reference, and source-relatedness decisions, with +`spellpayload` holding their shared parser and lookup helpers. -Production composition is grouped behind package-family registrars. Generic -and Seriatim implementations use their domain-first trees; D&D implementations -remain in their current stage-oriented packages: +Production composition is grouped behind package-family registrars, and every +implemented production extension uses its domain-first tree: | Package | Implemented responsibility | | --- | --- | diff --git a/internal/cli/compatibility_test.go b/internal/cli/compatibility_test.go index 92d86c4..92511bc 100644 --- a/internal/cli/compatibility_test.go +++ b/internal/cli/compatibility_test.go @@ -18,13 +18,13 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" frameworkllm "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/modules/chunk/dnd/scenes" - "gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" + spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape" + spellsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/source_refs" + spellrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/source_relatedness" validjson "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json" validjsonschema "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json_schema" - spellshape "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/shape" - spellsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/source_refs" - spellrelatedness "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/source_relatedness" ) func TestProductionCompatibilitySnapshot(t *testing.T) { diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index 191a99c..d240dc9 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -22,8 +22,11 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/modules/chunk/dnd/scenes" - "gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" + spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape" + spellsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/source_refs" + spellrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/source_relatedness" "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/chunk/units" "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/merge/appendorder" "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop" @@ -33,9 +36,6 @@ import ( validjson "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json" validjsonschema "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json_schema" "gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript" - spellshape "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/shape" - spellsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/source_refs" - spellrelatedness "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/source_relatedness" "gitea.maximumdirect.net/eric/scriptorium" ) diff --git a/internal/modules/sharedassets/prompt_fs.go b/internal/framework/promptfs/prompt_fs.go similarity index 99% rename from internal/modules/sharedassets/prompt_fs.go rename to internal/framework/promptfs/prompt_fs.go index 5ba030f..eb6f6c5 100644 --- a/internal/modules/sharedassets/prompt_fs.go +++ b/internal/framework/promptfs/prompt_fs.go @@ -1,4 +1,4 @@ -package sharedassets +package promptfs import ( "bytes" diff --git a/internal/modules/sharedassets/prompt_fs_test.go b/internal/framework/promptfs/prompt_fs_test.go similarity index 99% rename from internal/modules/sharedassets/prompt_fs_test.go rename to internal/framework/promptfs/prompt_fs_test.go index 6605f0d..38577a9 100644 --- a/internal/modules/sharedassets/prompt_fs_test.go +++ b/internal/framework/promptfs/prompt_fs_test.go @@ -1,4 +1,4 @@ -package sharedassets +package promptfs import ( "io/fs" diff --git a/internal/modules/chunk/dnd/scenes/model.go b/internal/modules/chunk/dnd/scenes/model.go deleted file mode 100644 index 1918b65..0000000 --- a/internal/modules/chunk/dnd/scenes/model.go +++ /dev/null @@ -1,30 +0,0 @@ -package scenes - -import "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" - -type chunkResponse struct { - Scenes []sceneResponse `json:"scenes"` - BoundaryCaveats []string `json:"boundary_caveats"` -} - -type sceneResponse struct { - StartUnitID dnd.UnitRef `json:"start_unit_id"` - EndUnitID dnd.UnitRef `json:"end_unit_id"` - ShortTitle string `json:"short_title"` - PrimaryMode string `json:"primary_mode"` - MainParticipants []string `json:"main_participants"` - Summary string `json:"summary"` - BoundaryNote string `json:"boundary_note"` - BoundaryConfidence string `json:"boundary_confidence"` -} - -type normalizedScene struct { - StartUnitID int - EndUnitID int - ShortTitle string - PrimaryMode string - MainParticipants []string - Summary string - BoundaryNote string - BoundaryConfidence string -} diff --git a/internal/modules/chunk/dnd/scenes/assets.go b/internal/modules/dnd/chunk/scenes/assets.go similarity index 100% rename from internal/modules/chunk/dnd/scenes/assets.go rename to internal/modules/dnd/chunk/scenes/assets.go diff --git a/internal/modules/chunk/dnd/scenes/assets/prompts/dnd.scenes.yaml b/internal/modules/dnd/chunk/scenes/assets/prompts/dnd.scenes.yaml similarity index 100% rename from internal/modules/chunk/dnd/scenes/assets/prompts/dnd.scenes.yaml rename to internal/modules/dnd/chunk/scenes/assets/prompts/dnd.scenes.yaml diff --git a/internal/modules/chunk/dnd/scenes/assets/prompts/instructions.md b/internal/modules/dnd/chunk/scenes/assets/prompts/instructions.md similarity index 100% rename from internal/modules/chunk/dnd/scenes/assets/prompts/instructions.md rename to internal/modules/dnd/chunk/scenes/assets/prompts/instructions.md diff --git a/internal/modules/chunk/dnd/scenes/assets/prompts/task.md b/internal/modules/dnd/chunk/scenes/assets/prompts/task.md similarity index 100% rename from internal/modules/chunk/dnd/scenes/assets/prompts/task.md rename to internal/modules/dnd/chunk/scenes/assets/prompts/task.md diff --git a/internal/modules/chunk/dnd/scenes/assets/schemas/dnd_scenes.v1.json b/internal/modules/dnd/chunk/scenes/assets/schemas/dnd_scenes.v1.json similarity index 100% rename from internal/modules/chunk/dnd/scenes/assets/schemas/dnd_scenes.v1.json rename to internal/modules/dnd/chunk/scenes/assets/schemas/dnd_scenes.v1.json diff --git a/internal/modules/chunk/dnd/scenes/chunker.go b/internal/modules/dnd/chunk/scenes/chunker.go similarity index 95% rename from internal/modules/chunk/dnd/scenes/chunker.go rename to internal/modules/dnd/chunk/scenes/chunker.go index e84ad06..3fe9f82 100644 --- a/internal/modules/chunk/dnd/scenes/chunker.go +++ b/internal/modules/dnd/chunk/scenes/chunker.go @@ -9,7 +9,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) const Key = "dnd/scenes" @@ -23,7 +23,7 @@ var providedCapabilities = []string{ "chunks.scenes", } -var referenceSlotDescriptions = dnd.ReferenceSlotDescriptions{ +var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ Glossary: "Optional campaign glossary reference material used only for scene disambiguation.", Party: "Optional party roster reference material used only for scene disambiguation.", Players: "Optional player list reference material used only for scene disambiguation.", @@ -44,7 +44,7 @@ func (c *Chunker) Key() string { } func (c *Chunker) ReferenceSlots() []contracts.ReferenceSlot { - return dnd.ReferenceSlots(referenceSlotDescriptions) + return shared.ReferenceSlots(referenceSlotDescriptions) } func (c *Chunker) ManifestMetadata() map[string]any { @@ -100,7 +100,7 @@ func (c *Chunker) Chunk(ctx context.Context, req contracts.ChunkRequest) (contra PromptVersion: ResponseSchemaVersion, ProfileID: req.LLMProfile, SessionID: req.SessionID, - Inputs: dnd.PromptInputs(req.SourceInput, req.References), + Inputs: shared.PromptInputs(req.SourceInput, req.References), }, &response); err != nil { return contracts.ChunkResult{}, chunkerErrorf("complete structured output: %w", err) } @@ -125,7 +125,7 @@ func ModuleSpec() pipeline.ModuleSpec { Stage: pipeline.StageChunk, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), - ReferenceSlots: dnd.ReferenceSlots(referenceSlotDescriptions), + ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions), } } @@ -228,11 +228,11 @@ func chunkContent(units []source.SourceUnit) ([]byte, error) { } func normalizeScene(doc *source.SourceDocument, index int, scene sceneResponse) (normalizedScene, error) { - startUnitID, err := dnd.ResolveUnitID(doc, "start_unit_id", scene.StartUnitID) + startUnitID, err := shared.ResolveUnitID(doc, "start_unit_id", scene.StartUnitID) if err != nil { return normalizedScene{}, fmt.Errorf("scene[%d] %w", index, err) } - endUnitID, err := dnd.ResolveUnitID(doc, "end_unit_id", scene.EndUnitID) + endUnitID, err := shared.ResolveUnitID(doc, "end_unit_id", scene.EndUnitID) if err != nil { return normalizedScene{}, fmt.Errorf("scene[%d] %w", index, err) } diff --git a/internal/modules/chunk/dnd/scenes/chunker_test.go b/internal/modules/dnd/chunk/scenes/chunker_test.go similarity index 96% rename from internal/modules/chunk/dnd/scenes/chunker_test.go rename to internal/modules/dnd/chunk/scenes/chunker_test.go index fdb9da9..3681723 100644 --- a/internal/modules/chunk/dnd/scenes/chunker_test.go +++ b/internal/modules/dnd/chunk/scenes/chunker_test.go @@ -11,7 +11,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) func TestNewModuleSpecAndRegister(t *testing.T) { @@ -105,8 +105,8 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) { response: chunkResponse{ Scenes: []sceneResponse{ { - StartUnitID: dnd.UnitRefFromInt(1), - EndUnitID: dnd.UnitRefFromInt(2), + StartUnitID: shared.UnitRefFromInt(1), + EndUnitID: shared.UnitRefFromInt(2), ShortTitle: " Goblin parley ", PrimaryMode: "Discussion", MainParticipants: []string{" Aria ", "Goblin scout"}, @@ -115,8 +115,8 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) { BoundaryConfidence: "High", }, { - StartUnitID: dnd.UnitRefFromInt(3), - EndUnitID: dnd.UnitRefFromInt(4), + StartUnitID: shared.UnitRefFromInt(3), + EndUnitID: shared.UnitRefFromInt(4), ShortTitle: "Ambush at the gate", PrimaryMode: "Combat", MainParticipants: []string{"Aria", "Goblin ambushers"}, @@ -210,8 +210,8 @@ func TestChunkPassesReferencesAsPromptInputs(t *testing.T) { client := &fakeScenesLLMClient{response: chunkResponse{ Scenes: []sceneResponse{ { - StartUnitID: dnd.UnitRefFromInt(1), - EndUnitID: dnd.UnitRefFromInt(4), + StartUnitID: shared.UnitRefFromInt(1), + EndUnitID: shared.UnitRefFromInt(4), ShortTitle: "Ambush", PrimaryMode: "Combat", MainParticipants: []string{"Aria"}, @@ -264,7 +264,7 @@ func TestChunkPassesReferencesAsPromptInputs(t *testing.T) { } func TestPromptInputsMapLegacyRosterReferenceToParty(t *testing.T) { - inputs := dnd.PromptInputs(sceneSourceInput(), contracts.ReferenceSet{ + inputs := shared.PromptInputs(sceneSourceInput(), contracts.ReferenceSet{ Slots: map[string]contracts.ResolvedReferenceSlot{ "roster": { Slot: contracts.ReferenceSlot{Name: "roster"}, @@ -451,8 +451,8 @@ func TestChunkRejectsMalformedStructuredOutput(t *testing.T) { name: "empty metadata field", response: replaceScenes(validSceneResponse(), []sceneResponse{ { - StartUnitID: dnd.UnitRefFromInt(1), - EndUnitID: dnd.UnitRefFromInt(4), + StartUnitID: shared.UnitRefFromInt(1), + EndUnitID: shared.UnitRefFromInt(4), ShortTitle: " ", PrimaryMode: "Narrative", MainParticipants: []string{"Aria"}, @@ -467,8 +467,8 @@ func TestChunkRejectsMalformedStructuredOutput(t *testing.T) { name: "empty participant", response: replaceScenes(validSceneResponse(), []sceneResponse{ { - StartUnitID: dnd.UnitRefFromInt(1), - EndUnitID: dnd.UnitRefFromInt(4), + StartUnitID: shared.UnitRefFromInt(1), + EndUnitID: shared.UnitRefFromInt(4), ShortTitle: "Title", PrimaryMode: "Narrative", MainParticipants: []string{"Aria", " "}, @@ -559,8 +559,8 @@ func replaceScenes(response chunkResponse, scenes []sceneResponse) chunkResponse func scene(startUnitID int, endUnitID int) sceneResponse { return sceneResponse{ - StartUnitID: dnd.UnitRefFromInt(startUnitID), - EndUnitID: dnd.UnitRefFromInt(endUnitID), + StartUnitID: shared.UnitRefFromInt(startUnitID), + EndUnitID: shared.UnitRefFromInt(endUnitID), ShortTitle: "Scene title", PrimaryMode: "Narrative", MainParticipants: []string{"Aria"}, diff --git a/internal/modules/dnd/chunk/scenes/model.go b/internal/modules/dnd/chunk/scenes/model.go new file mode 100644 index 0000000..a8778d4 --- /dev/null +++ b/internal/modules/dnd/chunk/scenes/model.go @@ -0,0 +1,30 @@ +package scenes + +import "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" + +type chunkResponse struct { + Scenes []sceneResponse `json:"scenes"` + BoundaryCaveats []string `json:"boundary_caveats"` +} + +type sceneResponse struct { + StartUnitID shared.UnitRef `json:"start_unit_id"` + EndUnitID shared.UnitRef `json:"end_unit_id"` + ShortTitle string `json:"short_title"` + PrimaryMode string `json:"primary_mode"` + MainParticipants []string `json:"main_participants"` + Summary string `json:"summary"` + BoundaryNote string `json:"boundary_note"` + BoundaryConfidence string `json:"boundary_confidence"` +} + +type normalizedScene struct { + StartUnitID int + EndUnitID int + ShortTitle string + PrimaryMode string + MainParticipants []string + Summary string + BoundaryNote string + BoundaryConfidence string +} diff --git a/internal/modules/chunk/dnd/scenes/schema.go b/internal/modules/dnd/chunk/scenes/schema.go similarity index 100% rename from internal/modules/chunk/dnd/scenes/schema.go rename to internal/modules/dnd/chunk/scenes/schema.go diff --git a/internal/modules/chunk/dnd/scenes/schema_test.go b/internal/modules/dnd/chunk/scenes/schema_test.go similarity index 100% rename from internal/modules/chunk/dnd/scenes/schema_test.go rename to internal/modules/dnd/chunk/scenes/schema_test.go diff --git a/internal/modules/chunk/dnd/scenes/scriptorium_assets.go b/internal/modules/dnd/chunk/scenes/scriptorium_assets.go similarity index 79% rename from internal/modules/chunk/dnd/scenes/scriptorium_assets.go rename to internal/modules/dnd/chunk/scenes/scriptorium_assets.go index 05a7e7e..f00cfe5 100644 --- a/internal/modules/chunk/dnd/scenes/scriptorium_assets.go +++ b/internal/modules/dnd/chunk/scenes/scriptorium_assets.go @@ -5,14 +5,14 @@ import ( "sync" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) const scriptoriumPromptRoot = "assets/prompts" func RegisterPromptAssets(registry *llm.AssetRegistry) error { - promptFS, err := dnd.ModulePromptFS("dnd.scenes", embeddedAssets, []sharedassets.ModulePromptFile{ + promptFS, err := shared.ModulePromptFS("dnd.scenes", embeddedAssets, []promptfs.ModulePromptFile{ {Name: "dnd.scenes.yaml", Path: "assets/prompts/dnd.scenes.yaml"}, {Name: "task.md", Path: "assets/prompts/task.md"}, {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, @@ -32,7 +32,7 @@ func scriptoriumPromptMetadata() (string, error) { {FS: embeddedAssets, Path: "assets/prompts/dnd.scenes.yaml"}, {FS: embeddedAssets, Path: "assets/prompts/task.md"}, {FS: embeddedAssets, Path: "assets/prompts/instructions.md"}, - }, append(dnd.CommonHashParts(), dnd.ReferenceHashParts()...)...) + }, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...) scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts) }) return scriptoriumPromptHash, scriptoriumPromptHashErr diff --git a/internal/modules/chunk/dnd/scenes/scriptorium_assets_test.go b/internal/modules/dnd/chunk/scenes/scriptorium_assets_test.go similarity index 100% rename from internal/modules/chunk/dnd/scenes/scriptorium_assets_test.go rename to internal/modules/dnd/chunk/scenes/scriptorium_assets_test.go diff --git a/internal/modules/extract/dnd/spells/assets.go b/internal/modules/dnd/extract/spells/assets.go similarity index 100% rename from internal/modules/extract/dnd/spells/assets.go rename to internal/modules/dnd/extract/spells/assets.go diff --git a/internal/modules/extract/dnd/spells/assets/prompts/dnd.spells.yaml b/internal/modules/dnd/extract/spells/assets/prompts/dnd.spells.yaml similarity index 100% rename from internal/modules/extract/dnd/spells/assets/prompts/dnd.spells.yaml rename to internal/modules/dnd/extract/spells/assets/prompts/dnd.spells.yaml diff --git a/internal/modules/extract/dnd/spells/assets/prompts/instructions.md b/internal/modules/dnd/extract/spells/assets/prompts/instructions.md similarity index 100% rename from internal/modules/extract/dnd/spells/assets/prompts/instructions.md rename to internal/modules/dnd/extract/spells/assets/prompts/instructions.md diff --git a/internal/modules/extract/dnd/spells/assets/prompts/task.md b/internal/modules/dnd/extract/spells/assets/prompts/task.md similarity index 100% rename from internal/modules/extract/dnd/spells/assets/prompts/task.md rename to internal/modules/dnd/extract/spells/assets/prompts/task.md diff --git a/internal/modules/extract/dnd/spells/assets/schemas/dnd_spells.v1.json b/internal/modules/dnd/extract/spells/assets/schemas/dnd_spells.v1.json similarity index 100% rename from internal/modules/extract/dnd/spells/assets/schemas/dnd_spells.v1.json rename to internal/modules/dnd/extract/spells/assets/schemas/dnd_spells.v1.json diff --git a/internal/modules/extract/dnd/spells/assets/schemas/dnd_spells_llm.v1.json b/internal/modules/dnd/extract/spells/assets/schemas/dnd_spells_llm.v1.json similarity index 100% rename from internal/modules/extract/dnd/spells/assets/schemas/dnd_spells_llm.v1.json rename to internal/modules/dnd/extract/spells/assets/schemas/dnd_spells_llm.v1.json diff --git a/internal/modules/extract/dnd/spells/canonicalize.go b/internal/modules/dnd/extract/spells/canonicalize.go similarity index 82% rename from internal/modules/extract/dnd/spells/canonicalize.go rename to internal/modules/dnd/extract/spells/canonicalize.go index 8e74973..a1f8128 100644 --- a/internal/modules/extract/dnd/spells/canonicalize.go +++ b/internal/modules/dnd/extract/spells/canonicalize.go @@ -3,7 +3,7 @@ package spells import ( "sort" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) func canonicalizeResponse(response *extractionResponse, sourceID string) { @@ -43,20 +43,20 @@ func canonicalizeSpellCast(spell *spellCastResponse, sourceID string) { spell.SourceRefs = dedupeSourceRefs(spell.SourceRefs) } -func canonicalUnitRef(ref dnd.UnitRef) dnd.UnitRef { +func canonicalUnitRef(ref shared.UnitRef) shared.UnitRef { value := ref.Int() if value <= 0 { return ref } - return dnd.UnitRefFromInt(value) + return shared.UnitRefFromInt(value) } -func dedupeSourceRefs(refs []dnd.SourceRefResponse) []dnd.SourceRefResponse { +func dedupeSourceRefs(refs []shared.SourceRefResponse) []shared.SourceRefResponse { if len(refs) < 2 { return refs } out := refs[:0] - var previous dnd.SourceRefResponse + var previous shared.SourceRefResponse for index, ref := range refs { if index > 0 && sameSourceRef(previous, ref) { continue @@ -67,7 +67,7 @@ func dedupeSourceRefs(refs []dnd.SourceRefResponse) []dnd.SourceRefResponse { return out } -func sameSourceRef(left dnd.SourceRefResponse, right dnd.SourceRefResponse) bool { +func sameSourceRef(left shared.SourceRefResponse, right shared.SourceRefResponse) bool { return left.SourceID == right.SourceID && left.StartUnitID.Int() == right.StartUnitID.Int() && left.EndUnitID.Int() == right.EndUnitID.Int() @@ -83,7 +83,7 @@ func earliestSourceUnit(spell spellCastResponse) (int, bool) { return 0, false } -func unitSortValue(ref dnd.UnitRef) int { +func unitSortValue(ref shared.UnitRef) int { value := ref.Int() if value <= 0 { return int(^uint(0) >> 1) diff --git a/internal/modules/extract/dnd/spells/extractor.go b/internal/modules/dnd/extract/spells/extractor.go similarity index 94% rename from internal/modules/extract/dnd/spells/extractor.go rename to internal/modules/dnd/extract/spells/extractor.go index 373c47a..fa9b665 100644 --- a/internal/modules/extract/dnd/spells/extractor.go +++ b/internal/modules/dnd/extract/spells/extractor.go @@ -8,7 +8,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) const Key = "dnd/spells" @@ -24,7 +24,7 @@ var providedCapabilities = []string{ "dnd.spell_casts", } -var referenceSlotDescriptions = dnd.ReferenceSlotDescriptions{ +var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{ Glossary: "Optional campaign glossary reference material used only for disambiguation.", Party: "Optional party roster reference material used only for disambiguation.", Players: "Optional player list reference material used only for disambiguation.", @@ -44,7 +44,7 @@ func (e *Extractor) Key() string { } func (e *Extractor) ReferenceSlots() []contracts.ReferenceSlot { - return dnd.ReferenceSlots(referenceSlotDescriptions) + return shared.ReferenceSlots(referenceSlotDescriptions) } func (e *Extractor) ManifestMetadata() map[string]any { @@ -101,7 +101,7 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest PromptVersion: SchemaVersion, ProfileID: req.LLMProfile, SessionID: req.SessionID, - Inputs: dnd.PromptInputs(sourceInput, req.References), + Inputs: shared.PromptInputs(sourceInput, req.References), }, &response); err != nil { return contracts.ExtractionResult{}, extractorErrorf("complete structured output: %w", err) } @@ -159,7 +159,7 @@ func ModuleSpec() pipeline.ModuleSpec { Stage: pipeline.StageExtract, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), - ReferenceSlots: dnd.ReferenceSlots(referenceSlotDescriptions), + ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions), } } diff --git a/internal/modules/extract/dnd/spells/extractor_test.go b/internal/modules/dnd/extract/spells/extractor_test.go similarity index 95% rename from internal/modules/extract/dnd/spells/extractor_test.go rename to internal/modules/dnd/extract/spells/extractor_test.go index 2371959..41341bb 100644 --- a/internal/modules/extract/dnd/spells/extractor_test.go +++ b/internal/modules/dnd/extract/spells/extractor_test.go @@ -8,7 +8,7 @@ import ( "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) func TestExtractReturnsCanonicalOutputFromStructuredResponse(t *testing.T) { @@ -155,7 +155,7 @@ func TestExtractPassesReferencesAsPromptInputs(t *testing.T) { } func TestPromptInputsMapLegacyRosterReferenceToParty(t *testing.T) { - inputs := dnd.PromptInputs(spellSourceInput(), contracts.ReferenceSet{ + inputs := shared.PromptInputs(spellSourceInput(), contracts.ReferenceSet{ Slots: map[string]contracts.ResolvedReferenceSlot{ "roster": { Slot: contracts.ReferenceSlot{Name: "roster"}, @@ -303,10 +303,10 @@ func TestExtractCanonicalizesSourceRefs(t *testing.T) { Spell: "Cure Wounds", Effect: "Heals.", NarrativeDescription: "Aria heals.", - SourceRefs: []dnd.SourceRefResponse{ - {SourceID: "gameplay_transcript", StartUnitID: dnd.UnitRefFromInt(2), EndUnitID: dnd.UnitRefFromInt(2)}, - {SourceID: "", StartUnitID: dnd.UnitRefFromInt(1), EndUnitID: dnd.UnitRefFromInt(2)}, - {SourceID: "transcript", StartUnitID: dnd.UnitRefFromInt(1), EndUnitID: dnd.UnitRefFromInt(2)}, + SourceRefs: []shared.SourceRefResponse{ + {SourceID: "gameplay_transcript", StartUnitID: shared.UnitRefFromInt(2), EndUnitID: shared.UnitRefFromInt(2)}, + {SourceID: "", StartUnitID: shared.UnitRefFromInt(1), EndUnitID: shared.UnitRefFromInt(2)}, + {SourceID: "transcript", StartUnitID: shared.UnitRefFromInt(1), EndUnitID: shared.UnitRefFromInt(2)}, }, }, }, @@ -345,8 +345,8 @@ func TestExtractPreservesInvalidSourceRefsForValidators(t *testing.T) { Spell: "Cure Wounds", Effect: "Heals.", NarrativeDescription: "Aria heals.", - SourceRefs: []dnd.SourceRefResponse{ - {SourceID: "transcript", StartUnitID: dnd.UnitRefFromInt(99), EndUnitID: dnd.UnitRefFromString("missing")}, + SourceRefs: []shared.SourceRefResponse{ + {SourceID: "transcript", StartUnitID: shared.UnitRefFromInt(99), EndUnitID: shared.UnitRefFromString("missing")}, }, }, }, @@ -386,7 +386,7 @@ func TestExtractDefensivelyCopiesRawContent(t *testing.T) { if err != nil { t.Fatalf("Extract() error = %v, want nil", err) } - client.response.SpellCasts[0].SourceRefs[0].StartUnitID = dnd.UnitRefFromInt(99) + client.response.SpellCasts[0].SourceRefs[0].StartUnitID = shared.UnitRefFromInt(99) var payload extractionResponse if err := json.Unmarshal(result.Output.Payload.Content, &payload); err != nil { diff --git a/internal/modules/dnd/extract/spells/model.go b/internal/modules/dnd/extract/spells/model.go new file mode 100644 index 0000000..278457e --- /dev/null +++ b/internal/modules/dnd/extract/spells/model.go @@ -0,0 +1,22 @@ +package spells + +import "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" + +type SpellCast struct { + Caster string `json:"caster"` + Spell string `json:"spell"` + Effect string `json:"effect"` + NarrativeDescription string `json:"narrative_description"` +} + +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 []shared.SourceRefResponse `json:"source_refs"` +} diff --git a/internal/modules/extract/dnd/spells/registry_test.go b/internal/modules/dnd/extract/spells/registry_test.go similarity index 100% rename from internal/modules/extract/dnd/spells/registry_test.go rename to internal/modules/dnd/extract/spells/registry_test.go diff --git a/internal/modules/extract/dnd/spells/schema.go b/internal/modules/dnd/extract/spells/schema.go similarity index 100% rename from internal/modules/extract/dnd/spells/schema.go rename to internal/modules/dnd/extract/spells/schema.go diff --git a/internal/modules/extract/dnd/spells/schema_test.go b/internal/modules/dnd/extract/spells/schema_test.go similarity index 100% rename from internal/modules/extract/dnd/spells/schema_test.go rename to internal/modules/dnd/extract/spells/schema_test.go diff --git a/internal/modules/extract/dnd/spells/scriptorium_assets.go b/internal/modules/dnd/extract/spells/scriptorium_assets.go similarity index 79% rename from internal/modules/extract/dnd/spells/scriptorium_assets.go rename to internal/modules/dnd/extract/spells/scriptorium_assets.go index 3cd7119..d2d0d64 100644 --- a/internal/modules/extract/dnd/spells/scriptorium_assets.go +++ b/internal/modules/dnd/extract/spells/scriptorium_assets.go @@ -5,14 +5,14 @@ import ( "sync" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) const scriptoriumPromptRoot = "assets/prompts" func RegisterPromptAssets(registry *llm.AssetRegistry) error { - promptFS, err := dnd.ModulePromptFS("dnd.spells", embeddedAssets, []sharedassets.ModulePromptFile{ + promptFS, err := shared.ModulePromptFS("dnd.spells", embeddedAssets, []promptfs.ModulePromptFile{ {Name: "dnd.spells.yaml", Path: "assets/prompts/dnd.spells.yaml"}, {Name: "task.md", Path: "assets/prompts/task.md"}, {Name: "instructions.md", Path: "assets/prompts/instructions.md"}, @@ -32,7 +32,7 @@ func scriptoriumPromptMetadata() (string, error) { {FS: embeddedAssets, Path: "assets/prompts/dnd.spells.yaml"}, {FS: embeddedAssets, Path: "assets/prompts/task.md"}, {FS: embeddedAssets, Path: "assets/prompts/instructions.md"}, - }, append(dnd.CommonHashParts(), dnd.ReferenceHashParts()...)...) + }, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...) scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts) }) return scriptoriumPromptHash, scriptoriumPromptHashErr diff --git a/internal/modules/extract/dnd/spells/scriptorium_assets_test.go b/internal/modules/dnd/extract/spells/scriptorium_assets_test.go similarity index 100% rename from internal/modules/extract/dnd/spells/scriptorium_assets_test.go rename to internal/modules/dnd/extract/spells/scriptorium_assets_test.go diff --git a/internal/modules/extract/dnd/spells/test_helpers_test.go b/internal/modules/dnd/extract/spells/test_helpers_test.go similarity index 81% rename from internal/modules/extract/dnd/spells/test_helpers_test.go rename to internal/modules/dnd/extract/spells/test_helpers_test.go index 460b410..7597c06 100644 --- a/internal/modules/extract/dnd/spells/test_helpers_test.go +++ b/internal/modules/dnd/extract/spells/test_helpers_test.go @@ -6,7 +6,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) func promptExtractionRequest() contracts.ExtractionRequest { @@ -65,22 +65,22 @@ func mustJSON(t *testing.T, value any) string { return string(encoded) } -func responseSourceRefs(sourceID string, startUnitID int, endUnitID int) []dnd.SourceRefResponse { - return []dnd.SourceRefResponse{ +func responseSourceRefs(sourceID string, startUnitID int, endUnitID int) []shared.SourceRefResponse { + return []shared.SourceRefResponse{ { SourceID: sourceID, - StartUnitID: dnd.UnitRefFromInt(startUnitID), - EndUnitID: dnd.UnitRefFromInt(endUnitID), + StartUnitID: shared.UnitRefFromInt(startUnitID), + EndUnitID: shared.UnitRefFromInt(endUnitID), }, } } -func responseSourceRefsInt(sourceID string, startUnitID int, endUnitID int) []dnd.SourceRefResponse { - return []dnd.SourceRefResponse{ +func responseSourceRefsInt(sourceID string, startUnitID int, endUnitID int) []shared.SourceRefResponse { + return []shared.SourceRefResponse{ { SourceID: sourceID, - StartUnitID: dnd.UnitRefFromInt(startUnitID), - EndUnitID: dnd.UnitRefFromInt(endUnitID), + StartUnitID: shared.UnitRefFromInt(startUnitID), + EndUnitID: shared.UnitRefFromInt(endUnitID), }, } } diff --git a/internal/modules/dnd/register/register.go b/internal/modules/dnd/register/register.go index 449bd18..bda7874 100644 --- a/internal/modules/dnd/register/register.go +++ b/internal/modules/dnd/register/register.go @@ -6,13 +6,13 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/modules/chunk/dnd/scenes" - "gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" + spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape" + spellsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/source_refs" + spellrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/source_relatedness" validjson "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json" validjsonschema "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json_schema" - spellshape "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/shape" - spellsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/source_refs" - spellrelatedness "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/source_relatedness" ) // Register adds all production D&D modules, validators, policy, and assets. diff --git a/internal/modules/dnd/register/register_test.go b/internal/modules/dnd/register/register_test.go index 385f1c9..23e0a3f 100644 --- a/internal/modules/dnd/register/register_test.go +++ b/internal/modules/dnd/register/register_test.go @@ -9,7 +9,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" ) func TestRegisterAddsDNDFamily(t *testing.T) { diff --git a/internal/modules/sharedassets/dnd/assets.go b/internal/modules/dnd/shared/assets.go similarity index 62% rename from internal/modules/sharedassets/dnd/assets.go rename to internal/modules/dnd/shared/assets.go index 8293376..9caebcd 100644 --- a/internal/modules/sharedassets/dnd/assets.go +++ b/internal/modules/dnd/shared/assets.go @@ -1,11 +1,11 @@ -package dnd +package shared import ( "embed" "io/fs" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets" + "gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" ) //go:embed assets/prompts/*.md @@ -17,10 +17,10 @@ var sharedPromptFiles = []string{ "common-dnd-references.md", } -func SharedPromptFiles() []sharedassets.SharedPromptFile { - files := make([]sharedassets.SharedPromptFile, 0, len(sharedPromptFiles)) +func SharedPromptFiles() []promptfs.SharedPromptFile { + files := make([]promptfs.SharedPromptFile, 0, len(sharedPromptFiles)) for _, name := range sharedPromptFiles { - files = append(files, sharedassets.SharedPromptFile{ + files = append(files, promptfs.SharedPromptFile{ Name: name, FS: embeddedAssets, Path: "assets/prompts/" + name, @@ -42,6 +42,6 @@ func ReferenceHashParts() []llm.AssetHashPart { } } -func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []sharedassets.ModulePromptFile) (fs.FS, error) { - return sharedassets.ModulePromptFS(moduleDir, moduleFS, files, SharedPromptFiles()...) +func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []promptfs.ModulePromptFile) (fs.FS, error) { + return promptfs.ModulePromptFS(moduleDir, moduleFS, files, SharedPromptFiles()...) } diff --git a/internal/modules/sharedassets/dnd/assets/prompts/common-dnd-references.md b/internal/modules/dnd/shared/assets/prompts/common-dnd-references.md similarity index 100% rename from internal/modules/sharedassets/dnd/assets/prompts/common-dnd-references.md rename to internal/modules/dnd/shared/assets/prompts/common-dnd-references.md diff --git a/internal/modules/sharedassets/dnd/assets/prompts/common-dnd-system.md b/internal/modules/dnd/shared/assets/prompts/common-dnd-system.md similarity index 100% rename from internal/modules/sharedassets/dnd/assets/prompts/common-dnd-system.md rename to internal/modules/dnd/shared/assets/prompts/common-dnd-system.md diff --git a/internal/modules/sharedassets/dnd/assets/prompts/common-dnd-transcript.md b/internal/modules/dnd/shared/assets/prompts/common-dnd-transcript.md similarity index 100% rename from internal/modules/sharedassets/dnd/assets/prompts/common-dnd-transcript.md rename to internal/modules/dnd/shared/assets/prompts/common-dnd-transcript.md diff --git a/internal/modules/sharedassets/dnd/assets_test.go b/internal/modules/dnd/shared/assets_test.go similarity index 95% rename from internal/modules/sharedassets/dnd/assets_test.go rename to internal/modules/dnd/shared/assets_test.go index 686ce6f..f02890c 100644 --- a/internal/modules/sharedassets/dnd/assets_test.go +++ b/internal/modules/dnd/shared/assets_test.go @@ -1,4 +1,4 @@ -package dnd +package shared import ( "io/fs" @@ -6,7 +6,7 @@ import ( "testing/fstest" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets" + "gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" ) func TestSharedPromptFilesReturnsNewSlice(t *testing.T) { @@ -67,7 +67,7 @@ func assertHashParts(t *testing.T, name string, parts []llm.AssetHashPart, want func TestModulePromptFSMountsDNDSharedPrompts(t *testing.T) { fsys, err := ModulePromptFS("dnd.test", fstest.MapFS{ "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, - }, []sharedassets.ModulePromptFile{ + }, []promptfs.ModulePromptFile{ {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, }) if err != nil { diff --git a/internal/modules/sharedassets/dnd/prompt_inputs.go b/internal/modules/dnd/shared/prompt_inputs.go similarity index 99% rename from internal/modules/sharedassets/dnd/prompt_inputs.go rename to internal/modules/dnd/shared/prompt_inputs.go index c5cd195..c67ccc3 100644 --- a/internal/modules/sharedassets/dnd/prompt_inputs.go +++ b/internal/modules/dnd/shared/prompt_inputs.go @@ -1,4 +1,4 @@ -package dnd +package shared import ( "bytes" diff --git a/internal/modules/sharedassets/dnd/prompt_inputs_test.go b/internal/modules/dnd/shared/prompt_inputs_test.go similarity index 99% rename from internal/modules/sharedassets/dnd/prompt_inputs_test.go rename to internal/modules/dnd/shared/prompt_inputs_test.go index 6e9b4a9..77b62ef 100644 --- a/internal/modules/sharedassets/dnd/prompt_inputs_test.go +++ b/internal/modules/dnd/shared/prompt_inputs_test.go @@ -1,4 +1,4 @@ -package dnd +package shared import ( "strings" diff --git a/internal/modules/sharedassets/dnd/references.go b/internal/modules/dnd/shared/references.go similarity index 98% rename from internal/modules/sharedassets/dnd/references.go rename to internal/modules/dnd/shared/references.go index 6a50a2d..d938817 100644 --- a/internal/modules/sharedassets/dnd/references.go +++ b/internal/modules/dnd/shared/references.go @@ -1,4 +1,4 @@ -package dnd +package shared import "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" diff --git a/internal/modules/sharedassets/dnd/references_test.go b/internal/modules/dnd/shared/references_test.go similarity index 99% rename from internal/modules/sharedassets/dnd/references_test.go rename to internal/modules/dnd/shared/references_test.go index 13f96fb..5a080a1 100644 --- a/internal/modules/sharedassets/dnd/references_test.go +++ b/internal/modules/dnd/shared/references_test.go @@ -1,4 +1,4 @@ -package dnd +package shared import ( "reflect" diff --git a/internal/modules/sharedassets/dnd/unit_refs.go b/internal/modules/dnd/shared/unit_refs.go similarity index 99% rename from internal/modules/sharedassets/dnd/unit_refs.go rename to internal/modules/dnd/shared/unit_refs.go index 96058b9..465046e 100644 --- a/internal/modules/sharedassets/dnd/unit_refs.go +++ b/internal/modules/dnd/shared/unit_refs.go @@ -1,4 +1,4 @@ -package dnd +package shared import ( "bytes" diff --git a/internal/modules/sharedassets/dnd/unit_refs_test.go b/internal/modules/dnd/shared/unit_refs_test.go similarity index 99% rename from internal/modules/sharedassets/dnd/unit_refs_test.go rename to internal/modules/dnd/shared/unit_refs_test.go index a07b046..93276f2 100644 --- a/internal/modules/sharedassets/dnd/unit_refs_test.go +++ b/internal/modules/dnd/shared/unit_refs_test.go @@ -1,4 +1,4 @@ -package dnd +package shared import ( "encoding/json" diff --git a/internal/validators/extract/dnd/spells/shape/validator.go b/internal/modules/dnd/validate/spells/shape/validator.go similarity index 93% rename from internal/validators/extract/dnd/spells/shape/validator.go rename to internal/modules/dnd/validate/spells/shape/validator.go index 41e20c3..cf1c116 100644 --- a/internal/validators/extract/dnd/spells/shape/validator.go +++ b/internal/modules/dnd/validate/spells/shape/validator.go @@ -5,7 +5,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/spellpayload" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/spellpayload" ) const Key = "extract/dnd/spells/shape" diff --git a/internal/validators/extract/dnd/spells/shape/validator_test.go b/internal/modules/dnd/validate/spells/shape/validator_test.go similarity index 100% rename from internal/validators/extract/dnd/spells/shape/validator_test.go rename to internal/modules/dnd/validate/spells/shape/validator_test.go diff --git a/internal/validators/extract/dnd/spells/source_refs/validator.go b/internal/modules/dnd/validate/spells/source_refs/validator.go similarity index 95% rename from internal/validators/extract/dnd/spells/source_refs/validator.go rename to internal/modules/dnd/validate/spells/source_refs/validator.go index 8d2c8d5..4e80523 100644 --- a/internal/validators/extract/dnd/spells/source_refs/validator.go +++ b/internal/modules/dnd/validate/spells/source_refs/validator.go @@ -7,7 +7,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/spellpayload" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/spellpayload" ) const Key = "extract/dnd/spells/source_refs" diff --git a/internal/validators/extract/dnd/spells/source_refs/validator_test.go b/internal/modules/dnd/validate/spells/source_refs/validator_test.go similarity index 100% rename from internal/validators/extract/dnd/spells/source_refs/validator_test.go rename to internal/modules/dnd/validate/spells/source_refs/validator_test.go diff --git a/internal/validators/extract/dnd/spells/source_relatedness/validator.go b/internal/modules/dnd/validate/spells/source_relatedness/validator.go similarity index 96% rename from internal/validators/extract/dnd/spells/source_relatedness/validator.go rename to internal/modules/dnd/validate/spells/source_relatedness/validator.go index b3b227a..ba5eca2 100644 --- a/internal/validators/extract/dnd/spells/source_relatedness/validator.go +++ b/internal/modules/dnd/validate/spells/source_relatedness/validator.go @@ -8,7 +8,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" - "gitea.maximumdirect.net/eric/notarius/internal/validators/extract/dnd/spells/spellpayload" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/spellpayload" ) const Key = "extract/dnd/spells/source_relatedness" diff --git a/internal/validators/extract/dnd/spells/source_relatedness/validator_test.go b/internal/modules/dnd/validate/spells/source_relatedness/validator_test.go similarity index 100% rename from internal/validators/extract/dnd/spells/source_relatedness/validator_test.go rename to internal/modules/dnd/validate/spells/source_relatedness/validator_test.go diff --git a/internal/validators/extract/dnd/spells/spellpayload/payload.go b/internal/modules/dnd/validate/spells/spellpayload/payload.go similarity index 83% rename from internal/validators/extract/dnd/spells/spellpayload/payload.go rename to internal/modules/dnd/validate/spells/spellpayload/payload.go index 904e1e8..36b7f97 100644 --- a/internal/validators/extract/dnd/spells/spellpayload/payload.go +++ b/internal/modules/dnd/validate/spells/spellpayload/payload.go @@ -9,7 +9,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" - "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) type Payload struct { @@ -17,11 +17,11 @@ type Payload struct { } type SpellCast struct { - Caster string `json:"caster"` - Spell string `json:"spell"` - Effect string `json:"effect"` - NarrativeDescription string `json:"narrative_description"` - SourceRefs []dnd.SourceRefResponse `json:"source_refs"` + Caster string `json:"caster"` + Spell string `json:"spell"` + Effect string `json:"effect"` + NarrativeDescription string `json:"narrative_description"` + SourceRefs []shared.SourceRefResponse `json:"source_refs"` } func Parse(raw []byte) (Payload, error) { @@ -66,7 +66,7 @@ func ValidateShape(payload Payload) error { func SourceRefCandidates(doc *source.SourceDocument, spell SpellCast) []source.SourceRef { refs := make([]source.SourceRef, 0, len(spell.SourceRefs)) for _, ref := range spell.SourceRefs { - refs = append(refs, dnd.SourceRefCandidate(doc, ref)) + refs = append(refs, shared.SourceRefCandidate(doc, ref)) } return refs } diff --git a/internal/modules/extract/dnd/spells/model.go b/internal/modules/extract/dnd/spells/model.go deleted file mode 100644 index 6980868..0000000 --- a/internal/modules/extract/dnd/spells/model.go +++ /dev/null @@ -1,22 +0,0 @@ -package spells - -import "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" - -type SpellCast struct { - Caster string `json:"caster"` - Spell string `json:"spell"` - Effect string `json:"effect"` - NarrativeDescription string `json:"narrative_description"` -} - -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 []dnd.SourceRefResponse `json:"source_refs"` -} diff --git a/internal/modules/generic/testdata/importboundaries/imports_dnd.go b/internal/modules/generic/testdata/importboundaries/imports_dnd.go new file mode 100644 index 0000000..1ffdc18 --- /dev/null +++ b/internal/modules/generic/testdata/importboundaries/imports_dnd.go @@ -0,0 +1,3 @@ +package importboundaries + +import _ "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" diff --git a/internal/modules/import_boundaries_test.go b/internal/modules/import_boundaries_test.go new file mode 100644 index 0000000..9affb16 --- /dev/null +++ b/internal/modules/import_boundaries_test.go @@ -0,0 +1,203 @@ +package modules_test + +import ( + "fmt" + "go/parser" + "go/token" + "io/fs" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" +) + +const moduleImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/modules/" + +func TestProductionImportBoundaries(t *testing.T) { + repositoryRoot := testRepositoryRoot(t) + err := filepath.WalkDir(repositoryRoot, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if entry.Name() == ".git" || entry.Name() == "testdata" || entry.Name() == "vendor" { + return filepath.SkipDir + } + return nil + } + if filepath.Ext(path) != ".go" { + return nil + } + return checkImportBoundaries(repositoryRoot, path) + }) + if err != nil { + t.Fatal(err) + } +} + +func TestImportBoundaryFixtureIsRejected(t *testing.T) { + repositoryRoot := testRepositoryRoot(t) + fixture := filepath.Join(repositoryRoot, "internal", "modules", "generic", "testdata", "importboundaries", "imports_dnd.go") + err := checkImportBoundaries(repositoryRoot, fixture) + if err == nil { + t.Fatal("fixture import was accepted, want generic-to-D&D violation") + } + if !strings.Contains(err.Error(), "generic packages must not import D&D packages") { + t.Fatalf("fixture error = %q, want generic-to-D&D violation", err) + } +} + +func TestImportBoundaryRules(t *testing.T) { + tests := []struct { + name string + filename string + importPath string + wantError bool + }{ + { + name: "D&D implementation cannot import Seriatim", + filename: "internal/modules/dnd/extract/example/extractor.go", + importPath: moduleImportPrefix + "seriatim/input/transcript", + wantError: true, + }, + { + name: "Seriatim implementation cannot import D&D", + filename: "internal/modules/seriatim/input/example/adapter.go", + importPath: moduleImportPrefix + "dnd/shared", + wantError: true, + }, + { + name: "generic implementation cannot import D&D", + filename: "internal/modules/generic/merge/example/merger.go", + importPath: moduleImportPrefix + "dnd/shared", + wantError: true, + }, + { + name: "domain root cannot import child", + filename: "internal/modules/dnd/types.go", + importPath: moduleImportPrefix + "dnd/extract/spells", + wantError: true, + }, + { + name: "D&D implementation may import generic implementation", + filename: "internal/modules/dnd/extract/example/extractor.go", + importPath: moduleImportPrefix + "generic/normalize/noop", + }, + { + name: "domain registrar may compose child packages", + filename: "internal/modules/dnd/register/register.go", + importPath: moduleImportPrefix + "dnd/extract/spells", + }, + { + name: "CLI may compose registrars", + filename: "internal/cli/catalog.go", + importPath: moduleImportPrefix + "dnd/register", + }, + { + name: "external integration test may compose domains", + filename: "internal/modules/integration/example_test.go", + importPath: moduleImportPrefix + "dnd/extract/spells", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateImport(tt.filename, tt.importPath) + if tt.wantError && err == nil { + t.Fatal("validateImport() error = nil, want boundary violation") + } + if !tt.wantError && err != nil { + t.Fatalf("validateImport() error = %v, want nil", err) + } + }) + } +} + +func checkImportBoundaries(repositoryRoot string, filename string) error { + parsed, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.ImportsOnly) + if err != nil { + return fmt.Errorf("parse %s: %w", filename, err) + } + relative, err := filepath.Rel(repositoryRoot, filename) + if err != nil { + return fmt.Errorf("resolve relative path for %s: %w", filename, err) + } + relative = filepath.ToSlash(relative) + for _, imported := range parsed.Imports { + importPath, err := strconv.Unquote(imported.Path.Value) + if err != nil { + return fmt.Errorf("parse import in %s: %w", relative, err) + } + if err := validateImport(relative, importPath); err != nil { + return fmt.Errorf("%s imports %s: %w", relative, importPath, err) + } + } + return nil +} + +func validateImport(filename string, importPath string) error { + if isExternalIntegrationTest(filename) { + return nil + } + sourceDomain, sourceRoot := domainForFile(filename) + targetDomain, targetChild := domainForImport(importPath) + if sourceDomain == "" || targetDomain == "" { + return nil + } + if sourceRoot && sourceDomain == targetDomain && targetChild { + return fmt.Errorf("domain root packages must not import child implementations") + } + if sourceDomain == "generic" && targetDomain == "dnd" { + return fmt.Errorf("generic packages must not import D&D packages") + } + if sourceDomain == "dnd" && targetDomain == "seriatim" { + return fmt.Errorf("D&D packages must not import Seriatim packages") + } + if sourceDomain == "seriatim" && targetDomain == "dnd" { + return fmt.Errorf("Seriatim packages must not import D&D packages") + } + return nil +} + +func domainForFile(filename string) (domain string, root bool) { + const prefix = "internal/modules/" + if !strings.HasPrefix(filename, prefix) { + return "", false + } + remainder := strings.TrimPrefix(filename, prefix) + parts := strings.Split(remainder, "/") + if len(parts) < 2 || !isDomain(parts[0]) { + return "", false + } + return parts[0], len(parts) == 2 +} + +func domainForImport(importPath string) (domain string, child bool) { + if !strings.HasPrefix(importPath, moduleImportPrefix) { + return "", false + } + remainder := strings.TrimPrefix(importPath, moduleImportPrefix) + parts := strings.Split(remainder, "/") + if len(parts) == 0 || !isDomain(parts[0]) { + return "", false + } + return parts[0], len(parts) > 1 +} + +func isDomain(name string) bool { + return name == "dnd" || name == "generic" || name == "seriatim" +} + +func isExternalIntegrationTest(filename string) bool { + return strings.HasPrefix(filename, "internal/modules/integration/") && strings.HasSuffix(filename, "_test.go") +} + +func testRepositoryRoot(t *testing.T) string { + t.Helper() + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("resolve import-boundary test location") + } + return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..")) +} diff --git a/internal/modules/extract/dnd/spells/config_test.go b/internal/modules/integration/dnd_spells_config_test.go similarity index 96% rename from internal/modules/extract/dnd/spells/config_test.go rename to internal/modules/integration/dnd_spells_config_test.go index 9ae674a..288ab9c 100644 --- a/internal/modules/extract/dnd/spells/config_test.go +++ b/internal/modules/integration/dnd_spells_config_test.go @@ -1,4 +1,4 @@ -package spells +package integration_test import ( "context" @@ -10,6 +10,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/merge/appendorder" "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop" "gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript" @@ -45,8 +46,8 @@ func TestPipelineConfigLoadsAndResolvesWithDNDSpellsExtractor(t *testing.T) { if lane.ID != "spells" { t.Fatalf("lane ID = %q, want spells", lane.ID) } - if lane.Extract.Module != Key { - t.Fatalf("extract module = %q, want %q", lane.Extract.Module, Key) + if lane.Extract.Module != spells.Key { + t.Fatalf("extract module = %q, want %q", lane.Extract.Module, spells.Key) } if resolved.ResolvedPipeline.Digest == "" { t.Fatal("resolved digest is empty") @@ -82,13 +83,13 @@ func TestPipelineConfigRejectsMissingTranscriptCapabilityForDNDSpells(t *testing } if !strings.Contains(err.Error(), "missing capability") || !strings.Contains(err.Error(), "source.transcript") || - !strings.Contains(err.Error(), Key) { + !strings.Contains(err.Error(), spells.Key) { t.Fatalf("Resolve() error = %q, want dnd/spells missing source.transcript capability", err.Error()) } } func TestPipelineConfigRejectsMissingSpellCastsCapabilityForAppendOrder(t *testing.T) { - extractorSpec := ModuleSpec() + extractorSpec := spells.ModuleSpec() extractorSpec.Provides = withoutCapability(extractorSpec.Provides, "dnd.spell_casts") _, err := loadDNDSpellsPipelineConfig(t).Resolve(config.ResolveInput{ @@ -176,11 +177,11 @@ func dndSpellsTestCatalog(t *testing.T, specs dndSpellsCatalogSpecs) pipeline.Mo } if specs.extractor.Key == "" { - if err := Register(extractors); err != nil { + if err := spells.Register(extractors); err != nil { t.Fatalf("register dnd spells extractor: %v", err) } } else if err := extractors.RegisterWithSpec(specs.extractor, func() (contracts.Extractor, error) { - return New(), nil + return spells.New(), nil }); err != nil { t.Fatalf("register dnd spells extractor override: %v", err) } diff --git a/internal/modules/integration/dnd_spells_helpers_test.go b/internal/modules/integration/dnd_spells_helpers_test.go new file mode 100644 index 0000000..276db48 --- /dev/null +++ b/internal/modules/integration/dnd_spells_helpers_test.go @@ -0,0 +1,63 @@ +package integration_test + +import ( + "context" + "encoding/json" + "fmt" + + "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" +) + +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 []shared.SourceRefResponse `json:"source_refs"` +} + +type fakeSpellsLLMClient struct { + response extractionResponse + requests []contracts.StructuredCompletionRequest +} + +func (client *fakeSpellsLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) { + client.requests = append(client.requests, cloneStructuredCompletionRequest(req)) + content, err := json.Marshal(client.response) + if err != nil { + return contracts.StructuredCompletionResponse{}, err + } + if err := json.Unmarshal(content, out); err != nil { + return contracts.StructuredCompletionResponse{}, fmt.Errorf("populate structured target: %w", err) + } + return contracts.StructuredCompletionResponse{Content: content}, nil +} + +func responseSourceRefs(sourceID string, startUnitID int, endUnitID int) []shared.SourceRefResponse { + return []shared.SourceRefResponse{ + { + SourceID: sourceID, + StartUnitID: shared.UnitRefFromInt(startUnitID), + EndUnitID: shared.UnitRefFromInt(endUnitID), + }, + } +} + +func cloneStructuredCompletionRequest(req contracts.StructuredCompletionRequest) contracts.StructuredCompletionRequest { + req.Inputs = req.Inputs.Clone() + if len(req.Vars) == 0 { + req.Vars = nil + return req + } + vars := make(map[string]any, len(req.Vars)) + for key, value := range req.Vars { + vars[key] = value + } + req.Vars = vars + return req +} diff --git a/internal/modules/extract/dnd/spells/runner_test.go b/internal/modules/integration/dnd_spells_runner_test.go similarity index 93% rename from internal/modules/extract/dnd/spells/runner_test.go rename to internal/modules/integration/dnd_spells_runner_test.go index f8ad91f..ba858ee 100644 --- a/internal/modules/extract/dnd/spells/runner_test.go +++ b/internal/modules/integration/dnd_spells_runner_test.go @@ -1,4 +1,4 @@ -package spells +package integration_test import ( "context" @@ -11,6 +11,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" + "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells" "gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript" ) @@ -52,7 +53,7 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) { t.Fatalf("len(NormalizeOutputs) = %d, want 1", len(output.NormalizeOutputs)) } rawOutput := output.NormalizeOutputs[0] - if rawOutput.LaneID != "spells" || rawOutput.Schema.ID != ResponseSchemaID || rawOutput.Schema.Version != SchemaVersion { + if rawOutput.LaneID != "spells" || rawOutput.Schema.ID != spells.ResponseSchemaID || rawOutput.Schema.Version != spells.SchemaVersion { t.Fatalf("raw output envelope = %#v, want dnd spells schema on spells lane", rawOutput) } response := decodeRunnerSpellResponse(t, rawOutput.Payload.Content) @@ -85,16 +86,16 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) { t.Fatalf("len(ArtifactLanes) = %d, want 1", len(output.Manifest.ArtifactLanes)) } lane := output.Manifest.ArtifactLanes[0] - if lane.ID != "spells" || lane.Extractor != Key { + if lane.ID != "spells" || lane.Extractor != spells.Key { t.Fatalf("manifest lane = %#v, want spells lane with dnd/spells extractor", lane) } extractorMetadata, ok := lane.Metadata["extractor"].(map[string]any) if !ok { t.Fatalf("manifest lane metadata = %#v, want extractor metadata", lane.Metadata) } - if extractorMetadata["prompt_id"] != PromptID || - extractorMetadata["response_schema_key"] != string(ResponseSchemaKey) || - extractorMetadata["response_schema_name"] != ResponseSchemaName { + if extractorMetadata["prompt_id"] != spells.PromptID || + extractorMetadata["response_schema_key"] != string(spells.ResponseSchemaKey) || + extractorMetadata["response_schema_name"] != spells.ResponseSchemaName { t.Fatalf("extractor metadata = %#v, want prompt/schema identifiers", extractorMetadata) } if len(output.OutputFiles) != 1 { @@ -146,8 +147,8 @@ func TestRunnerPassesPartyAndGlossaryReferencesToDNDSpellsPrompt(t *testing.T) { t.Fatalf("LLM calls = %d, want 1", len(llmClient.requests)) } request := llmClient.requests[0] - if request.PromptID != PromptID || request.PromptVersion != SchemaVersion { - t.Fatalf("prompt = %q/%q, want %q/%q", request.PromptID, request.PromptVersion, PromptID, SchemaVersion) + if request.PromptID != spells.PromptID || request.PromptVersion != spells.SchemaVersion { + t.Fatalf("prompt = %q/%q, want %q/%q", request.PromptID, request.PromptVersion, spells.PromptID, spells.SchemaVersion) } if got := string(request.Inputs["party"].Content); got != "Aria: party cleric\nBorin: fighter" { t.Fatalf("party input = %q, want reference text", got) @@ -188,8 +189,8 @@ func TestRunnerDoesNotExtractSpellMentionedOnlyInPartyReference(t *testing.T) { t.Fatalf("LLM calls = %d, want 1", len(llmClient.requests)) } request := llmClient.requests[0] - if request.PromptID != PromptID || request.PromptVersion != SchemaVersion { - t.Fatalf("prompt = %q/%q, want %q/%q", request.PromptID, request.PromptVersion, PromptID, SchemaVersion) + if request.PromptID != spells.PromptID || request.PromptVersion != spells.SchemaVersion { + t.Fatalf("prompt = %q/%q, want %q/%q", request.PromptID, request.PromptVersion, spells.PromptID, spells.SchemaVersion) } if got := string(request.Inputs["party"].Content); !strings.Contains(got, "Lightning Bolt") { t.Fatalf("party input = %q, want party-reference-only spell in reference input", got) diff --git a/internal/modules/extract/dnd/spells/testdata/pipeline.yml b/internal/modules/integration/testdata/pipeline.yml similarity index 100% rename from internal/modules/extract/dnd/spells/testdata/pipeline.yml rename to internal/modules/integration/testdata/pipeline.yml diff --git a/internal/modules/extract/dnd/spells/testdata/seriatim_spell_session.json b/internal/modules/integration/testdata/seriatim_spell_session.json similarity index 100% rename from internal/modules/extract/dnd/spells/testdata/seriatim_spell_session.json rename to internal/modules/integration/testdata/seriatim_spell_session.json