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
|
`AssetRegistry` combines caller-owned prompt filesystems under stable prefixes
|
||||||
and rejects invalid or conflicting registrations. Production module packages
|
and rejects invalid or conflicting registrations. Production module packages
|
||||||
register their own prompt and schema assets; generic framework code contains no
|
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,
|
Schema helpers load embedded JSON Schema with identity and digest metadata,
|
||||||
return defensive copies, and expose a diagnostics map that omits schema bytes.
|
return defensive copies, and expose a diagnostics map that omits schema bytes.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Module And Validator Internals
|
# Module And Validator Internals
|
||||||
|
|
||||||
Production module implementations and generic validators live under
|
Production module and validator implementations live under their domain-first
|
||||||
`internal/modules`; the current D&D validators live under `internal/validators`.
|
trees in `internal/modules`.
|
||||||
The selectable keys, configuration options, reference slots, and default
|
The selectable keys, configuration options, reference slots, and default
|
||||||
validator chain are canonical in the
|
validator chain are canonical in the
|
||||||
[module](../config.md#implemented-production-modules) and
|
[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
|
LLM-backed extensions own their prompt definitions and response schemas under
|
||||||
package-local embedded assets. Shared filesystem composition belongs in
|
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
|
declarations, prompt-input assembly, and source-unit helpers belong in
|
||||||
`internal/modules/sharedassets/dnd`. Stage contracts expose only Notarius
|
`internal/modules/dnd/shared`. Stage contracts expose only Notarius structured-
|
||||||
structured-completion types, not Scriptorium public types.
|
completion types, not Scriptorium public types.
|
||||||
|
|
||||||
Reference material may inform a module or prompt but must not become source
|
Reference material may inform a module or prompt but must not become source
|
||||||
evidence. The resolver and materializer behavior is described in
|
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
|
[Configuration](../config.md#implemented-production-modules). Generic
|
||||||
framework validation canonicalizes the returned unit slices before extraction.
|
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
|
The scene chunker prepares a structured Scriptorium request from the full
|
||||||
transcript, session, and optional D&D reference inputs. It validates the model's
|
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
|
## Extractor
|
||||||
|
|
||||||
### `internal/modules/extract/dnd/spells`
|
### `internal/modules/dnd/extract/spells`
|
||||||
|
|
||||||
The spell extractor prepares a structured request from one chunk, the
|
The spell extractor prepares a structured request from one chunk, the
|
||||||
chunk-scoped source input, the session, and optional D&D reference inputs. It
|
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
|
## 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
|
shape checks, source-reference candidates, and cited-text lookup shared by the
|
||||||
three validators.
|
three validators.
|
||||||
|
|
||||||
@@ -199,5 +199,7 @@ does not inventory implementations.
|
|||||||
defaults.
|
defaults.
|
||||||
- `internal/cli/run_test.go`: production catalog, config resolution, and
|
- `internal/cli/run_test.go`: production catalog, config resolution, and
|
||||||
end-to-end CLI composition.
|
end-to-end CLI composition.
|
||||||
- `internal/modules/sharedassets/**/*_test.go`: shared prompt and reference
|
- `internal/framework/promptfs/*_test.go` and
|
||||||
assembly.
|
`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/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/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/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/checkpoint` | Workspace-backed checkpoint loading, recording, and payload serialization. |
|
||||||
| `internal/framework/debug` | Workspace-backed framework and LLM debug recording. |
|
| `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/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/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/dnd/chunk/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/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/merge/appendorder` | Combines accepted extraction results in chunk order. |
|
||||||
| `internal/modules/generic/normalize/noop` | Preserves accepted merged output. |
|
| `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/generic/output/json` | Encodes manifests, lane payloads, warnings, and rejections as logical JSON files. |
|
||||||
|
|
||||||
`internal/modules/sharedassets` composes shared prompt filesystems.
|
`internal/modules/dnd/shared` owns reusable D&D prompt fragments,
|
||||||
`internal/modules/sharedassets/dnd` owns reusable D&D prompt fragments,
|
|
||||||
reference declarations, prompt input assembly, and source-unit reference
|
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
|
Generic validators under `internal/modules/generic/validate` provide
|
||||||
unconditional test decisions, JSON syntax validation, and JSON Schema
|
unconditional test decisions, JSON syntax validation, and JSON Schema
|
||||||
validation. D&D spell validators remain under `internal/validators` and provide
|
validation. D&D spell validators under `internal/modules/dnd/validate/spells`
|
||||||
shape, source-reference, and source-relatedness decisions, with `spellpayload`
|
provide shape, source-reference, and source-relatedness decisions, with
|
||||||
holding their shared parser and lookup helpers.
|
`spellpayload` holding their shared parser and lookup helpers.
|
||||||
|
|
||||||
Production composition is grouped behind package-family registrars. Generic
|
Production composition is grouped behind package-family registrars, and every
|
||||||
and Seriatim implementations use their domain-first trees; D&D implementations
|
implemented production extension uses its domain-first tree:
|
||||||
remain in their current stage-oriented packages:
|
|
||||||
|
|
||||||
| Package | Implemented responsibility |
|
| Package | Implemented responsibility |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
frameworkllm "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
frameworkllm "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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/dnd/chunk/scenes"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells"
|
"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"
|
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"
|
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) {
|
func TestProductionCompatibilitySnapshot(t *testing.T) {
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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/dnd/chunk/scenes"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells"
|
"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/chunk/units"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/merge/appendorder"
|
"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/generic/normalize/noop"
|
||||||
@@ -33,9 +36,6 @@ import (
|
|||||||
validjson "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json"
|
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"
|
validjsonschema "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/validate/valid_json_schema"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
"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"
|
"gitea.maximumdirect.net/eric/scriptorium"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package sharedassets
|
package promptfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package sharedassets
|
package promptfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"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/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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"
|
const Key = "dnd/scenes"
|
||||||
@@ -23,7 +23,7 @@ var providedCapabilities = []string{
|
|||||||
"chunks.scenes",
|
"chunks.scenes",
|
||||||
}
|
}
|
||||||
|
|
||||||
var referenceSlotDescriptions = dnd.ReferenceSlotDescriptions{
|
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
|
||||||
Glossary: "Optional campaign glossary reference material used only for scene disambiguation.",
|
Glossary: "Optional campaign glossary reference material used only for scene disambiguation.",
|
||||||
Party: "Optional party roster 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.",
|
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 {
|
func (c *Chunker) ReferenceSlots() []contracts.ReferenceSlot {
|
||||||
return dnd.ReferenceSlots(referenceSlotDescriptions)
|
return shared.ReferenceSlots(referenceSlotDescriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chunker) ManifestMetadata() map[string]any {
|
func (c *Chunker) ManifestMetadata() map[string]any {
|
||||||
@@ -100,7 +100,7 @@ func (c *Chunker) Chunk(ctx context.Context, req contracts.ChunkRequest) (contra
|
|||||||
PromptVersion: ResponseSchemaVersion,
|
PromptVersion: ResponseSchemaVersion,
|
||||||
ProfileID: req.LLMProfile,
|
ProfileID: req.LLMProfile,
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
Inputs: dnd.PromptInputs(req.SourceInput, req.References),
|
Inputs: shared.PromptInputs(req.SourceInput, req.References),
|
||||||
}, &response); err != nil {
|
}, &response); err != nil {
|
||||||
return contracts.ChunkResult{}, chunkerErrorf("complete structured output: %w", err)
|
return contracts.ChunkResult{}, chunkerErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ func ModuleSpec() pipeline.ModuleSpec {
|
|||||||
Stage: pipeline.StageChunk,
|
Stage: pipeline.StageChunk,
|
||||||
Requires: append([]string(nil), requiredCapabilities...),
|
Requires: append([]string(nil), requiredCapabilities...),
|
||||||
Provides: append([]string(nil), providedCapabilities...),
|
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) {
|
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 {
|
if err != nil {
|
||||||
return normalizedScene{}, fmt.Errorf("scene[%d] %w", index, err)
|
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 {
|
if err != nil {
|
||||||
return normalizedScene{}, fmt.Errorf("scene[%d] %w", index, err)
|
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/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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) {
|
func TestNewModuleSpecAndRegister(t *testing.T) {
|
||||||
@@ -105,8 +105,8 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
|||||||
response: chunkResponse{
|
response: chunkResponse{
|
||||||
Scenes: []sceneResponse{
|
Scenes: []sceneResponse{
|
||||||
{
|
{
|
||||||
StartUnitID: dnd.UnitRefFromInt(1),
|
StartUnitID: shared.UnitRefFromInt(1),
|
||||||
EndUnitID: dnd.UnitRefFromInt(2),
|
EndUnitID: shared.UnitRefFromInt(2),
|
||||||
ShortTitle: " Goblin parley ",
|
ShortTitle: " Goblin parley ",
|
||||||
PrimaryMode: "Discussion",
|
PrimaryMode: "Discussion",
|
||||||
MainParticipants: []string{" Aria ", "Goblin scout"},
|
MainParticipants: []string{" Aria ", "Goblin scout"},
|
||||||
@@ -115,8 +115,8 @@ func TestChunkReturnsSceneChunksFromStructuredOutput(t *testing.T) {
|
|||||||
BoundaryConfidence: "High",
|
BoundaryConfidence: "High",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
StartUnitID: dnd.UnitRefFromInt(3),
|
StartUnitID: shared.UnitRefFromInt(3),
|
||||||
EndUnitID: dnd.UnitRefFromInt(4),
|
EndUnitID: shared.UnitRefFromInt(4),
|
||||||
ShortTitle: "Ambush at the gate",
|
ShortTitle: "Ambush at the gate",
|
||||||
PrimaryMode: "Combat",
|
PrimaryMode: "Combat",
|
||||||
MainParticipants: []string{"Aria", "Goblin ambushers"},
|
MainParticipants: []string{"Aria", "Goblin ambushers"},
|
||||||
@@ -210,8 +210,8 @@ func TestChunkPassesReferencesAsPromptInputs(t *testing.T) {
|
|||||||
client := &fakeScenesLLMClient{response: chunkResponse{
|
client := &fakeScenesLLMClient{response: chunkResponse{
|
||||||
Scenes: []sceneResponse{
|
Scenes: []sceneResponse{
|
||||||
{
|
{
|
||||||
StartUnitID: dnd.UnitRefFromInt(1),
|
StartUnitID: shared.UnitRefFromInt(1),
|
||||||
EndUnitID: dnd.UnitRefFromInt(4),
|
EndUnitID: shared.UnitRefFromInt(4),
|
||||||
ShortTitle: "Ambush",
|
ShortTitle: "Ambush",
|
||||||
PrimaryMode: "Combat",
|
PrimaryMode: "Combat",
|
||||||
MainParticipants: []string{"Aria"},
|
MainParticipants: []string{"Aria"},
|
||||||
@@ -264,7 +264,7 @@ func TestChunkPassesReferencesAsPromptInputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPromptInputsMapLegacyRosterReferenceToParty(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{
|
Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||||
"roster": {
|
"roster": {
|
||||||
Slot: contracts.ReferenceSlot{Name: "roster"},
|
Slot: contracts.ReferenceSlot{Name: "roster"},
|
||||||
@@ -451,8 +451,8 @@ func TestChunkRejectsMalformedStructuredOutput(t *testing.T) {
|
|||||||
name: "empty metadata field",
|
name: "empty metadata field",
|
||||||
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
||||||
{
|
{
|
||||||
StartUnitID: dnd.UnitRefFromInt(1),
|
StartUnitID: shared.UnitRefFromInt(1),
|
||||||
EndUnitID: dnd.UnitRefFromInt(4),
|
EndUnitID: shared.UnitRefFromInt(4),
|
||||||
ShortTitle: " ",
|
ShortTitle: " ",
|
||||||
PrimaryMode: "Narrative",
|
PrimaryMode: "Narrative",
|
||||||
MainParticipants: []string{"Aria"},
|
MainParticipants: []string{"Aria"},
|
||||||
@@ -467,8 +467,8 @@ func TestChunkRejectsMalformedStructuredOutput(t *testing.T) {
|
|||||||
name: "empty participant",
|
name: "empty participant",
|
||||||
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
response: replaceScenes(validSceneResponse(), []sceneResponse{
|
||||||
{
|
{
|
||||||
StartUnitID: dnd.UnitRefFromInt(1),
|
StartUnitID: shared.UnitRefFromInt(1),
|
||||||
EndUnitID: dnd.UnitRefFromInt(4),
|
EndUnitID: shared.UnitRefFromInt(4),
|
||||||
ShortTitle: "Title",
|
ShortTitle: "Title",
|
||||||
PrimaryMode: "Narrative",
|
PrimaryMode: "Narrative",
|
||||||
MainParticipants: []string{"Aria", " "},
|
MainParticipants: []string{"Aria", " "},
|
||||||
@@ -559,8 +559,8 @@ func replaceScenes(response chunkResponse, scenes []sceneResponse) chunkResponse
|
|||||||
|
|
||||||
func scene(startUnitID int, endUnitID int) sceneResponse {
|
func scene(startUnitID int, endUnitID int) sceneResponse {
|
||||||
return sceneResponse{
|
return sceneResponse{
|
||||||
StartUnitID: dnd.UnitRefFromInt(startUnitID),
|
StartUnitID: shared.UnitRefFromInt(startUnitID),
|
||||||
EndUnitID: dnd.UnitRefFromInt(endUnitID),
|
EndUnitID: shared.UnitRefFromInt(endUnitID),
|
||||||
ShortTitle: "Scene title",
|
ShortTitle: "Scene title",
|
||||||
PrimaryMode: "Narrative",
|
PrimaryMode: "Narrative",
|
||||||
MainParticipants: []string{"Aria"},
|
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"
|
"sync"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
const scriptoriumPromptRoot = "assets/prompts"
|
const scriptoriumPromptRoot = "assets/prompts"
|
||||||
|
|
||||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
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: "dnd.scenes.yaml", Path: "assets/prompts/dnd.scenes.yaml"},
|
||||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||||
{Name: "instructions.md", Path: "assets/prompts/instructions.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/dnd.scenes.yaml"},
|
||||||
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||||
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||||
}, append(dnd.CommonHashParts(), dnd.ReferenceHashParts()...)...)
|
}, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...)
|
||||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||||
})
|
})
|
||||||
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
||||||
@@ -3,7 +3,7 @@ package spells
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"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) {
|
func canonicalizeResponse(response *extractionResponse, sourceID string) {
|
||||||
@@ -43,20 +43,20 @@ func canonicalizeSpellCast(spell *spellCastResponse, sourceID string) {
|
|||||||
spell.SourceRefs = dedupeSourceRefs(spell.SourceRefs)
|
spell.SourceRefs = dedupeSourceRefs(spell.SourceRefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalUnitRef(ref dnd.UnitRef) dnd.UnitRef {
|
func canonicalUnitRef(ref shared.UnitRef) shared.UnitRef {
|
||||||
value := ref.Int()
|
value := ref.Int()
|
||||||
if value <= 0 {
|
if value <= 0 {
|
||||||
return ref
|
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 {
|
if len(refs) < 2 {
|
||||||
return refs
|
return refs
|
||||||
}
|
}
|
||||||
out := refs[:0]
|
out := refs[:0]
|
||||||
var previous dnd.SourceRefResponse
|
var previous shared.SourceRefResponse
|
||||||
for index, ref := range refs {
|
for index, ref := range refs {
|
||||||
if index > 0 && sameSourceRef(previous, ref) {
|
if index > 0 && sameSourceRef(previous, ref) {
|
||||||
continue
|
continue
|
||||||
@@ -67,7 +67,7 @@ func dedupeSourceRefs(refs []dnd.SourceRefResponse) []dnd.SourceRefResponse {
|
|||||||
return out
|
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 &&
|
return left.SourceID == right.SourceID &&
|
||||||
left.StartUnitID.Int() == right.StartUnitID.Int() &&
|
left.StartUnitID.Int() == right.StartUnitID.Int() &&
|
||||||
left.EndUnitID.Int() == right.EndUnitID.Int()
|
left.EndUnitID.Int() == right.EndUnitID.Int()
|
||||||
@@ -83,7 +83,7 @@ func earliestSourceUnit(spell spellCastResponse) (int, bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func unitSortValue(ref dnd.UnitRef) int {
|
func unitSortValue(ref shared.UnitRef) int {
|
||||||
value := ref.Int()
|
value := ref.Int()
|
||||||
if value <= 0 {
|
if value <= 0 {
|
||||||
return int(^uint(0) >> 1)
|
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/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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"
|
const Key = "dnd/spells"
|
||||||
@@ -24,7 +24,7 @@ var providedCapabilities = []string{
|
|||||||
"dnd.spell_casts",
|
"dnd.spell_casts",
|
||||||
}
|
}
|
||||||
|
|
||||||
var referenceSlotDescriptions = dnd.ReferenceSlotDescriptions{
|
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
|
||||||
Glossary: "Optional campaign glossary reference material used only for disambiguation.",
|
Glossary: "Optional campaign glossary reference material used only for disambiguation.",
|
||||||
Party: "Optional party roster 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.",
|
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 {
|
func (e *Extractor) ReferenceSlots() []contracts.ReferenceSlot {
|
||||||
return dnd.ReferenceSlots(referenceSlotDescriptions)
|
return shared.ReferenceSlots(referenceSlotDescriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Extractor) ManifestMetadata() map[string]any {
|
func (e *Extractor) ManifestMetadata() map[string]any {
|
||||||
@@ -101,7 +101,7 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest
|
|||||||
PromptVersion: SchemaVersion,
|
PromptVersion: SchemaVersion,
|
||||||
ProfileID: req.LLMProfile,
|
ProfileID: req.LLMProfile,
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
Inputs: dnd.PromptInputs(sourceInput, req.References),
|
Inputs: shared.PromptInputs(sourceInput, req.References),
|
||||||
}, &response); err != nil {
|
}, &response); err != nil {
|
||||||
return contracts.ExtractionResult{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.ExtractionResult{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ func ModuleSpec() pipeline.ModuleSpec {
|
|||||||
Stage: pipeline.StageExtract,
|
Stage: pipeline.StageExtract,
|
||||||
Requires: append([]string(nil), requiredCapabilities...),
|
Requires: append([]string(nil), requiredCapabilities...),
|
||||||
Provides: append([]string(nil), providedCapabilities...),
|
Provides: append([]string(nil), providedCapabilities...),
|
||||||
ReferenceSlots: dnd.ReferenceSlots(referenceSlotDescriptions),
|
ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"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) {
|
func TestExtractReturnsCanonicalOutputFromStructuredResponse(t *testing.T) {
|
||||||
@@ -155,7 +155,7 @@ func TestExtractPassesReferencesAsPromptInputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPromptInputsMapLegacyRosterReferenceToParty(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{
|
Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||||
"roster": {
|
"roster": {
|
||||||
Slot: contracts.ReferenceSlot{Name: "roster"},
|
Slot: contracts.ReferenceSlot{Name: "roster"},
|
||||||
@@ -303,10 +303,10 @@ func TestExtractCanonicalizesSourceRefs(t *testing.T) {
|
|||||||
Spell: "Cure Wounds",
|
Spell: "Cure Wounds",
|
||||||
Effect: "Heals.",
|
Effect: "Heals.",
|
||||||
NarrativeDescription: "Aria heals.",
|
NarrativeDescription: "Aria heals.",
|
||||||
SourceRefs: []dnd.SourceRefResponse{
|
SourceRefs: []shared.SourceRefResponse{
|
||||||
{SourceID: "gameplay_transcript", StartUnitID: dnd.UnitRefFromInt(2), EndUnitID: dnd.UnitRefFromInt(2)},
|
{SourceID: "gameplay_transcript", StartUnitID: shared.UnitRefFromInt(2), EndUnitID: shared.UnitRefFromInt(2)},
|
||||||
{SourceID: "", StartUnitID: dnd.UnitRefFromInt(1), EndUnitID: dnd.UnitRefFromInt(2)},
|
{SourceID: "", StartUnitID: shared.UnitRefFromInt(1), EndUnitID: shared.UnitRefFromInt(2)},
|
||||||
{SourceID: "transcript", StartUnitID: dnd.UnitRefFromInt(1), EndUnitID: dnd.UnitRefFromInt(2)},
|
{SourceID: "transcript", StartUnitID: shared.UnitRefFromInt(1), EndUnitID: shared.UnitRefFromInt(2)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -345,8 +345,8 @@ func TestExtractPreservesInvalidSourceRefsForValidators(t *testing.T) {
|
|||||||
Spell: "Cure Wounds",
|
Spell: "Cure Wounds",
|
||||||
Effect: "Heals.",
|
Effect: "Heals.",
|
||||||
NarrativeDescription: "Aria heals.",
|
NarrativeDescription: "Aria heals.",
|
||||||
SourceRefs: []dnd.SourceRefResponse{
|
SourceRefs: []shared.SourceRefResponse{
|
||||||
{SourceID: "transcript", StartUnitID: dnd.UnitRefFromInt(99), EndUnitID: dnd.UnitRefFromString("missing")},
|
{SourceID: "transcript", StartUnitID: shared.UnitRefFromInt(99), EndUnitID: shared.UnitRefFromString("missing")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -386,7 +386,7 @@ func TestExtractDefensivelyCopiesRawContent(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Extract() error = %v, want nil", err)
|
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
|
var payload extractionResponse
|
||||||
if err := json.Unmarshal(result.Output.Payload.Content, &payload); err != nil {
|
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"
|
"sync"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
const scriptoriumPromptRoot = "assets/prompts"
|
const scriptoriumPromptRoot = "assets/prompts"
|
||||||
|
|
||||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
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: "dnd.spells.yaml", Path: "assets/prompts/dnd.spells.yaml"},
|
||||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||||
{Name: "instructions.md", Path: "assets/prompts/instructions.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/dnd.spells.yaml"},
|
||||||
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||||
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||||
}, append(dnd.CommonHashParts(), dnd.ReferenceHashParts()...)...)
|
}, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...)
|
||||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||||
})
|
})
|
||||||
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"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 {
|
func promptExtractionRequest() contracts.ExtractionRequest {
|
||||||
@@ -65,22 +65,22 @@ func mustJSON(t *testing.T, value any) string {
|
|||||||
return string(encoded)
|
return string(encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
func responseSourceRefs(sourceID string, startUnitID int, endUnitID int) []dnd.SourceRefResponse {
|
func responseSourceRefs(sourceID string, startUnitID int, endUnitID int) []shared.SourceRefResponse {
|
||||||
return []dnd.SourceRefResponse{
|
return []shared.SourceRefResponse{
|
||||||
{
|
{
|
||||||
SourceID: sourceID,
|
SourceID: sourceID,
|
||||||
StartUnitID: dnd.UnitRefFromInt(startUnitID),
|
StartUnitID: shared.UnitRefFromInt(startUnitID),
|
||||||
EndUnitID: dnd.UnitRefFromInt(endUnitID),
|
EndUnitID: shared.UnitRefFromInt(endUnitID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func responseSourceRefsInt(sourceID string, startUnitID int, endUnitID int) []dnd.SourceRefResponse {
|
func responseSourceRefsInt(sourceID string, startUnitID int, endUnitID int) []shared.SourceRefResponse {
|
||||||
return []dnd.SourceRefResponse{
|
return []shared.SourceRefResponse{
|
||||||
{
|
{
|
||||||
SourceID: sourceID,
|
SourceID: sourceID,
|
||||||
StartUnitID: dnd.UnitRefFromInt(startUnitID),
|
StartUnitID: shared.UnitRefFromInt(startUnitID),
|
||||||
EndUnitID: dnd.UnitRefFromInt(endUnitID),
|
EndUnitID: shared.UnitRefFromInt(endUnitID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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/dnd/chunk/scenes"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/extract/dnd/spells"
|
"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"
|
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"
|
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.
|
// 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/llm"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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) {
|
func TestRegisterAddsDNDFamily(t *testing.T) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
"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
|
//go:embed assets/prompts/*.md
|
||||||
@@ -17,10 +17,10 @@ var sharedPromptFiles = []string{
|
|||||||
"common-dnd-references.md",
|
"common-dnd-references.md",
|
||||||
}
|
}
|
||||||
|
|
||||||
func SharedPromptFiles() []sharedassets.SharedPromptFile {
|
func SharedPromptFiles() []promptfs.SharedPromptFile {
|
||||||
files := make([]sharedassets.SharedPromptFile, 0, len(sharedPromptFiles))
|
files := make([]promptfs.SharedPromptFile, 0, len(sharedPromptFiles))
|
||||||
for _, name := range sharedPromptFiles {
|
for _, name := range sharedPromptFiles {
|
||||||
files = append(files, sharedassets.SharedPromptFile{
|
files = append(files, promptfs.SharedPromptFile{
|
||||||
Name: name,
|
Name: name,
|
||||||
FS: embeddedAssets,
|
FS: embeddedAssets,
|
||||||
Path: "assets/prompts/" + name,
|
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) {
|
func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []promptfs.ModulePromptFile) (fs.FS, error) {
|
||||||
return sharedassets.ModulePromptFS(moduleDir, moduleFS, files, SharedPromptFiles()...)
|
return promptfs.ModulePromptFS(moduleDir, moduleFS, files, SharedPromptFiles()...)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
"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) {
|
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) {
|
func TestModulePromptFSMountsDNDSharedPrompts(t *testing.T) {
|
||||||
fsys, err := ModulePromptFS("dnd.test", fstest.MapFS{
|
fsys, err := ModulePromptFS("dnd.test", fstest.MapFS{
|
||||||
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
|
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
|
||||||
}, []sharedassets.ModulePromptFile{
|
}, []promptfs.ModulePromptFile{
|
||||||
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
|
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
import "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dnd
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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"
|
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/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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"
|
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/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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"
|
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/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"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 {
|
type Payload struct {
|
||||||
@@ -21,7 +21,7 @@ type SpellCast struct {
|
|||||||
Spell string `json:"spell"`
|
Spell string `json:"spell"`
|
||||||
Effect string `json:"effect"`
|
Effect string `json:"effect"`
|
||||||
NarrativeDescription string `json:"narrative_description"`
|
NarrativeDescription string `json:"narrative_description"`
|
||||||
SourceRefs []dnd.SourceRefResponse `json:"source_refs"`
|
SourceRefs []shared.SourceRefResponse `json:"source_refs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(raw []byte) (Payload, error) {
|
func Parse(raw []byte) (Payload, error) {
|
||||||
@@ -66,7 +66,7 @@ func ValidateShape(payload Payload) error {
|
|||||||
func SourceRefCandidates(doc *source.SourceDocument, spell SpellCast) []source.SourceRef {
|
func SourceRefCandidates(doc *source.SourceDocument, spell SpellCast) []source.SourceRef {
|
||||||
refs := make([]source.SourceRef, 0, len(spell.SourceRefs))
|
refs := make([]source.SourceRef, 0, len(spell.SourceRefs))
|
||||||
for _, ref := range spell.SourceRefs {
|
for _, ref := range spell.SourceRefs {
|
||||||
refs = append(refs, dnd.SourceRefCandidate(doc, ref))
|
refs = append(refs, shared.SourceRefCandidate(doc, ref))
|
||||||
}
|
}
|
||||||
return refs
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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/merge/appendorder"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
||||||
@@ -45,8 +46,8 @@ func TestPipelineConfigLoadsAndResolvesWithDNDSpellsExtractor(t *testing.T) {
|
|||||||
if lane.ID != "spells" {
|
if lane.ID != "spells" {
|
||||||
t.Fatalf("lane ID = %q, want spells", lane.ID)
|
t.Fatalf("lane ID = %q, want spells", lane.ID)
|
||||||
}
|
}
|
||||||
if lane.Extract.Module != Key {
|
if lane.Extract.Module != spells.Key {
|
||||||
t.Fatalf("extract module = %q, want %q", lane.Extract.Module, Key)
|
t.Fatalf("extract module = %q, want %q", lane.Extract.Module, spells.Key)
|
||||||
}
|
}
|
||||||
if resolved.ResolvedPipeline.Digest == "" {
|
if resolved.ResolvedPipeline.Digest == "" {
|
||||||
t.Fatal("resolved digest is empty")
|
t.Fatal("resolved digest is empty")
|
||||||
@@ -82,13 +83,13 @@ func TestPipelineConfigRejectsMissingTranscriptCapabilityForDNDSpells(t *testing
|
|||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "missing capability") ||
|
if !strings.Contains(err.Error(), "missing capability") ||
|
||||||
!strings.Contains(err.Error(), "source.transcript") ||
|
!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())
|
t.Fatalf("Resolve() error = %q, want dnd/spells missing source.transcript capability", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPipelineConfigRejectsMissingSpellCastsCapabilityForAppendOrder(t *testing.T) {
|
func TestPipelineConfigRejectsMissingSpellCastsCapabilityForAppendOrder(t *testing.T) {
|
||||||
extractorSpec := ModuleSpec()
|
extractorSpec := spells.ModuleSpec()
|
||||||
extractorSpec.Provides = withoutCapability(extractorSpec.Provides, "dnd.spell_casts")
|
extractorSpec.Provides = withoutCapability(extractorSpec.Provides, "dnd.spell_casts")
|
||||||
|
|
||||||
_, err := loadDNDSpellsPipelineConfig(t).Resolve(config.ResolveInput{
|
_, err := loadDNDSpellsPipelineConfig(t).Resolve(config.ResolveInput{
|
||||||
@@ -176,11 +177,11 @@ func dndSpellsTestCatalog(t *testing.T, specs dndSpellsCatalogSpecs) pipeline.Mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if specs.extractor.Key == "" {
|
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)
|
t.Fatalf("register dnd spells extractor: %v", err)
|
||||||
}
|
}
|
||||||
} else if err := extractors.RegisterWithSpec(specs.extractor, func() (contracts.Extractor, error) {
|
} else if err := extractors.RegisterWithSpec(specs.extractor, func() (contracts.Extractor, error) {
|
||||||
return New(), nil
|
return spells.New(), nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("register dnd spells extractor override: %v", err)
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"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"
|
"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))
|
t.Fatalf("len(NormalizeOutputs) = %d, want 1", len(output.NormalizeOutputs))
|
||||||
}
|
}
|
||||||
rawOutput := output.NormalizeOutputs[0]
|
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)
|
t.Fatalf("raw output envelope = %#v, want dnd spells schema on spells lane", rawOutput)
|
||||||
}
|
}
|
||||||
response := decodeRunnerSpellResponse(t, rawOutput.Payload.Content)
|
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))
|
t.Fatalf("len(ArtifactLanes) = %d, want 1", len(output.Manifest.ArtifactLanes))
|
||||||
}
|
}
|
||||||
lane := output.Manifest.ArtifactLanes[0]
|
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)
|
t.Fatalf("manifest lane = %#v, want spells lane with dnd/spells extractor", lane)
|
||||||
}
|
}
|
||||||
extractorMetadata, ok := lane.Metadata["extractor"].(map[string]any)
|
extractorMetadata, ok := lane.Metadata["extractor"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("manifest lane metadata = %#v, want extractor metadata", lane.Metadata)
|
t.Fatalf("manifest lane metadata = %#v, want extractor metadata", lane.Metadata)
|
||||||
}
|
}
|
||||||
if extractorMetadata["prompt_id"] != PromptID ||
|
if extractorMetadata["prompt_id"] != spells.PromptID ||
|
||||||
extractorMetadata["response_schema_key"] != string(ResponseSchemaKey) ||
|
extractorMetadata["response_schema_key"] != string(spells.ResponseSchemaKey) ||
|
||||||
extractorMetadata["response_schema_name"] != ResponseSchemaName {
|
extractorMetadata["response_schema_name"] != spells.ResponseSchemaName {
|
||||||
t.Fatalf("extractor metadata = %#v, want prompt/schema identifiers", extractorMetadata)
|
t.Fatalf("extractor metadata = %#v, want prompt/schema identifiers", extractorMetadata)
|
||||||
}
|
}
|
||||||
if len(output.OutputFiles) != 1 {
|
if len(output.OutputFiles) != 1 {
|
||||||
@@ -146,8 +147,8 @@ func TestRunnerPassesPartyAndGlossaryReferencesToDNDSpellsPrompt(t *testing.T) {
|
|||||||
t.Fatalf("LLM calls = %d, want 1", len(llmClient.requests))
|
t.Fatalf("LLM calls = %d, want 1", len(llmClient.requests))
|
||||||
}
|
}
|
||||||
request := llmClient.requests[0]
|
request := llmClient.requests[0]
|
||||||
if request.PromptID != PromptID || request.PromptVersion != SchemaVersion {
|
if request.PromptID != spells.PromptID || request.PromptVersion != spells.SchemaVersion {
|
||||||
t.Fatalf("prompt = %q/%q, want %q/%q", request.PromptID, request.PromptVersion, PromptID, 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" {
|
if got := string(request.Inputs["party"].Content); got != "Aria: party cleric\nBorin: fighter" {
|
||||||
t.Fatalf("party input = %q, want reference text", got)
|
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))
|
t.Fatalf("LLM calls = %d, want 1", len(llmClient.requests))
|
||||||
}
|
}
|
||||||
request := llmClient.requests[0]
|
request := llmClient.requests[0]
|
||||||
if request.PromptID != PromptID || request.PromptVersion != SchemaVersion {
|
if request.PromptID != spells.PromptID || request.PromptVersion != spells.SchemaVersion {
|
||||||
t.Fatalf("prompt = %q/%q, want %q/%q", request.PromptID, request.PromptVersion, PromptID, 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") {
|
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)
|
t.Fatalf("party input = %q, want party-reference-only spell in reference input", got)
|
||||||
Reference in New Issue
Block a user