Organize D&D extensions by domain
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package sharedassets
|
||||
package promptfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package sharedassets
|
||||
package promptfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"},
|
||||
30
internal/modules/dnd/chunk/scenes/model.go
Normal file
30
internal/modules/dnd/chunk/scenes/model.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
22
internal/modules/dnd/extract/spells/model.go
Normal file
22
internal/modules/dnd/extract/spells/model.go
Normal file
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()...)
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
package dnd
|
||||
package shared
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package dnd
|
||||
package shared
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -1,4 +1,4 @@
|
||||
package dnd
|
||||
package shared
|
||||
|
||||
import "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package dnd
|
||||
package shared
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -1,4 +1,4 @@
|
||||
package dnd
|
||||
package shared
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package dnd
|
||||
package shared
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
3
internal/modules/generic/testdata/importboundaries/imports_dnd.go
vendored
Normal file
3
internal/modules/generic/testdata/importboundaries/imports_dnd.go
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
package importboundaries
|
||||
|
||||
import _ "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
||||
203
internal/modules/import_boundaries_test.go
Normal file
203
internal/modules/import_boundaries_test.go
Normal file
@@ -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), "..", ".."))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
63
internal/modules/integration/dnd_spells_helpers_test.go
Normal file
63
internal/modules/integration/dnd_spells_helpers_test.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user