Consolidate the D&D configuration examples

This commit is contained in:
2026-07-25 15:48:48 +00:00
parent 29ee68824d
commit 9614469b45
27 changed files with 296 additions and 313 deletions

View File

@@ -11,7 +11,7 @@ needed.
```sh ```sh
OPENROUTER_API_KEY=... \ OPENROUTER_API_KEY=... \
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json --input examples/seriatim-minimal-transcript.json
``` ```
@@ -29,7 +29,6 @@ Useful references:
- [D&D spell artifact contract](docs/integrations/dnd-spell-artifacts.md) - [D&D spell artifact contract](docs/integrations/dnd-spell-artifacts.md)
- [Developer guide](docs/development.md) - [Developer guide](docs/development.md)
- [Internal implementation docs](docs/internal/overview.md) - [Internal implementation docs](docs/internal/overview.md)
- [Maintained example config](examples/dnd-spells.config.yml) - [Minimal D&D configuration](examples/dnd-minimal.config.yml)
- [NPC-grounded example config](examples/dnd-npc-grounded.config.yml) - [Complete D&D configuration](examples/dnd-complete.config.yml)
- [NPC interaction example config](examples/dnd-npc-interactions.config.yml)
- [Maintained example input](examples/seriatim-minimal-transcript.json) - [Maintained example input](examples/seriatim-minimal-transcript.json)

View File

@@ -88,7 +88,7 @@ Use `slot=path` when the selected targets declare the slot unambiguously:
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--reference roster=./campaign-roster.txt --reference roster=./campaign-roster.txt
``` ```
@@ -98,7 +98,7 @@ when you want to target a specific target:
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--reference spells.extract.glossary=./campaign-glossary.txt --reference spells.extract.glossary=./campaign-glossary.txt
``` ```
@@ -109,8 +109,8 @@ it in memory and fans it out to spell extraction, combat extraction, and combat
normalization: normalization:
```sh ```sh
go run ./cmd/notarius run dnd-npc-grounded \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-npc-grounded.config.yml \ --config examples/dnd-complete.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--output-dir ./npc-grounded-output --output-dir ./npc-grounded-output
``` ```
@@ -135,7 +135,7 @@ Use `--without-reference` to remove a configured optional binding for a run:
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--without-reference glossary --without-reference glossary
``` ```
@@ -145,7 +145,7 @@ run to share an identifier:
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--session-id campaign-17-session-04 --session-id campaign-17-session-04
``` ```
@@ -156,7 +156,7 @@ recorded work; using it while checkpoint recording is disabled is an error:
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--resume --resume
``` ```
@@ -166,10 +166,10 @@ from the configuration. The selected step and dependents are reported as
`forced_recompute`; reusable predecessors are reported as `reused`: `forced_recompute`; reusable predecessors are reported as `reused`:
```sh ```sh
go run ./cmd/notarius run dnd-npc-grounded \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-npc-grounded.config.yml \ --config examples/dnd-complete.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--resume --recompute-step grounded-events --resume --recompute-step extract-events
``` ```
Checkpoint decisions use these categories: `reused`, `executed`, Checkpoint decisions use these categories: `reused`, `executed`,
@@ -187,7 +187,7 @@ code `1`.
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--debug --debug-dir ./notarius-debug --debug --debug-dir ./notarius-debug
``` ```
@@ -196,7 +196,7 @@ Use `refresh` when intentionally replacing the cached plan for the same source:
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--chunk_cache refresh --chunk_cache refresh
``` ```
@@ -205,7 +205,7 @@ Use `bypass` for a one-off run that must not inspect or create plan-cache state:
```sh ```sh
go run ./cmd/notarius run dnd-session \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--chunk_cache bypass --chunk_cache bypass
``` ```
@@ -230,10 +230,10 @@ Examples:
```sh ```sh
go run ./cmd/notarius config validate \ go run ./cmd/notarius config validate \
--config examples/dnd-spells.config.yml --config examples/dnd-minimal.config.yml
go run ./cmd/notarius config validate \ go run ./cmd/notarius config validate \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--pipeline dnd-session \ --pipeline dnd-session \
--only spells --only spells
``` ```
@@ -252,10 +252,10 @@ Examples:
```sh ```sh
go run ./cmd/notarius pipelines list \ go run ./cmd/notarius pipelines list \
--config examples/dnd-spells.config.yml --config examples/dnd-minimal.config.yml
go run ./cmd/notarius pipelines list \ go run ./cmd/notarius pipelines list \
--config examples/dnd-spells.config.yml \ --config examples/dnd-minimal.config.yml \
--json --json
``` ```

View File

@@ -19,15 +19,14 @@ The explicit-path option is defined in the [CLI reference](cli.md).
## Maintained Examples ## Maintained Examples
- [Minimal D&D spell configuration](../examples/dnd-spells.config.yml) - [Minimal D&D configuration](../examples/dnd-minimal.config.yml) shows the
- [Production-oriented D&D spell configuration](../examples/dnd-spells-production.config.yml) shortest useful single-step, single-lane pipeline.
- [D&D NPC configuration](../examples/dnd-npcs.config.yml) - [Complete D&D configuration](../examples/dnd-complete.config.yml) shows scene
- [D&D combat-turn configuration](../examples/dnd-combat-turns.config.yml) chunking, ordered steps, all five D&D artifact lanes, a generated NPC
- [D&D NPC-grounded spell and combat configuration](../examples/dnd-npc-grounded.config.yml) handoff, campaign and spell-catalog references, checkpoint storage, and
- [D&D NPC interaction configuration](../examples/dnd-npc-interactions.config.yml) chunk-map output.
- [D&D scene chunk-map configuration](../examples/dnd-scene-chunk-map.config.yml)
All are complete version 3 files. The fragments below illustrate individual Both are complete version 3 files. The fragments below illustrate individual
fields and are not alternate complete configurations. fields and are not alternate complete configurations.
## Top-Level Fields ## Top-Level Fields
@@ -347,9 +346,9 @@ output:
``` ```
The payload and index descriptor are defined by the The payload and index descriptor are defined by the
[Accepted Chunk Map contract](integrations/chunk-map.md). See the complete [Accepted Chunk Map contract](integrations/chunk-map.md). See the
[D&D scene chunk-map configuration](../examples/dnd-scene-chunk-map.config.yml) [complete D&D configuration](../examples/dnd-complete.config.yml) for a
for a copyable pipeline. copyable pipeline that enables this option.
Validator bindings use the same shorthand or object module-binding form, but Validator bindings use the same shorthand or object module-binding form, but
only these fields are supported: only these fields are supported:
@@ -563,17 +562,17 @@ bound.
The `dnd/npcs` extractor declares the same optional campaign slots as the spell The `dnd/npcs` extractor declares the same optional campaign slots as the spell
extractor, but it does not declare the `npcs` registry slot. Its normalizer extractor, but it does not declare the `npcs` registry slot. Its normalizer
accepts no references. The maintained accepts no references. The
[NPC-grounded example](../examples/dnd-npc-grounded.config.yml) binds its [complete D&D example](../examples/dnd-complete.config.yml) binds its accepted
accepted normalized output to the later spell and combat targets through an normalized output to later spell, combat-turn, and NPC-interaction targets
explicit ordered step. through an explicit ordered step.
The `dnd/scene-descriptions` extractor accepts only the optional `players`, The `dnd/scene-descriptions` extractor accepts only the optional `players`,
`party`, and `glossary` campaign slots. They can disambiguate terms in the `party`, and `glossary` campaign slots. They can disambiguate terms in the
model-facing title and summary but are never source evidence. Its normalizer model-facing title and summary but are never source evidence. Its normalizer
accepts no references. No NPC registry or generated artifact is required. See accepts no references. No NPC registry or generated artifact is required. See
the [D&D scene-description artifact contract](integrations/dnd-scene-description-artifacts.md) the [D&D scene-description artifact contract](integrations/dnd-scene-description-artifacts.md)
and the copyable [scene-description configuration](../examples/dnd-scene-descriptions.config.yml). and the copyable [complete D&D configuration](../examples/dnd-complete.config.yml).
The `dnd/combat-turns` extractor declares the optional campaign slots and the The `dnd/combat-turns` extractor declares the optional campaign slots and the
structured `npcs` slot. Campaign references guide only the LLM extraction structured `npcs` slot. Campaign references guide only the LLM extraction

View File

@@ -145,4 +145,4 @@ framework provenance and dependency fingerprints.
See [Configuration](../config.md#implemented-production-modules) for selectable See [Configuration](../config.md#implemented-production-modules) for selectable
keys and chains, [the NPC artifact contract](dnd-npc-artifacts.md) for the keys and chains, [the NPC artifact contract](dnd-npc-artifacts.md) for the
registry boundary, and the copyable registry boundary, and the copyable
[maintained example](../../examples/dnd-npc-interactions.config.yml). [complete D&D example](../../examples/dnd-complete.config.yml).

View File

@@ -115,6 +115,6 @@ bounded and do not copy transcript or campaign-reference content.
Use `dnd/scenes` to form accepted scene chunks, then select Use `dnd/scenes` to form accepted scene chunks, then select
`dnd/scene-descriptions` for extraction and normalization. The maintained `dnd/scene-descriptions` for extraction and normalization. The maintained
[scene-description configuration](../../examples/dnd-scene-descriptions.config.yml) [complete D&D configuration](../../examples/dnd-complete.config.yml) includes
is a copyable minimal lane. Selectable keys and default validator chains are this lane alongside the other D&D artifacts. Selectable keys and default
defined in [Configuration](../config.md). validator chains are defined in [Configuration](../config.md).

View File

@@ -48,15 +48,16 @@ needed. This is data deletion, not cache cleanup.
## Ordered D&D Workflow ## Ordered D&D Workflow
The maintained [NPC-grounded configuration](../examples/dnd-npc-grounded.config.yml) The maintained [complete D&D configuration](../examples/dnd-complete.config.yml)
contains one pipeline with two ordered steps. The first step extracts and contains one pipeline with two ordered steps. The first step extracts and
normalizes NPCs. Only after that lane reaches an accepted terminal result does normalizes NPCs and scene descriptions. Only after those lanes reach accepted
the second step begin; its generated NPC reference is supplied in memory to terminal results does the second step begin; its generated NPC reference is
spell extraction, combat extraction, and combat normalization. supplied in memory to spell, combat-turn, and NPC-interaction extraction and
the applicable normalizers.
```sh ```sh
go run ./cmd/notarius run dnd-npc-grounded \ go run ./cmd/notarius run dnd-session \
--config examples/dnd-npc-grounded.config.yml \ --config examples/dnd-complete.config.yml \
--input examples/seriatim-minimal-transcript.json \ --input examples/seriatim-minimal-transcript.json \
--output-dir ./npc-grounded-output --output-dir ./npc-grounded-output
``` ```

View File

@@ -1,22 +0,0 @@
version: 3
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: bypass
directory: ./notarius-cache/chunk-plans
checkpoints:
enabled: false
directory: ./notarius-cache/checkpoints
debug:
directory: ./notarius-debug
pipelines:
dnd-combat:
input: seriatim
chunk: generic
artifacts:
combat:
extract:
module: dnd/combat-turns
retries: 2
normalize: dnd/combat-turns

View File

@@ -0,0 +1,79 @@
version: 3
concurrency:
total_llm: 2
stage_workers:
extract: 2
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: auto
directory: ./notarius-cache/chunk-plans
checkpoints:
enabled: true
directory: ./notarius-cache/checkpoints
debug:
directory: ./notarius-debug
pipelines:
dnd-session:
input: seriatim
# Stable campaign context is shared by every module that accepts these slots.
references:
party: ./dnd-party.txt
glossary: ./dnd-glossary.txt
chunk:
module: dnd/scenes
retries: 2
output:
module: json
options:
include_chunk_map: true
steps:
# Establish session-wide reference artifacts before extracting events.
- id: describe-session
artifacts:
npcs:
extract:
module: dnd/npcs
retries: 2
merge: appendorder
normalize: dnd/npcs
scene-descriptions:
extract:
module: dnd/scene-descriptions
retries: 2
merge: appendorder
normalize: dnd/scene-descriptions
- id: extract-events
# The accepted NPC artifact is supplied in memory to every compatible
# extractor and normalizer in this step.
references:
npcs:
artifact:
step: describe-session
lane: npcs
artifacts:
spells:
extract:
module: dnd/spells
retries: 2
references:
spell_catalog: ./dnd-spell-catalog.json
merge: appendorder
# Stage-local file references are intentionally bound at each stage.
normalize:
module: dnd/spells
references:
spell_catalog: ./dnd-spell-catalog.json
combat-turns:
extract:
module: dnd/combat-turns
retries: 2
merge: appendorder
normalize: dnd/combat-turns
npc-interactions:
extract:
module: dnd/npc-interactions
retries: 2
merge: appendorder
normalize: dnd/npc-interactions

View File

@@ -0,0 +1,8 @@
version: 3
pipelines:
dnd-session:
input: seriatim
artifacts:
spells:
extract: dnd/spells
normalize: dnd/spells

View File

@@ -1,37 +0,0 @@
version: 3
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: false
directory: ""
debug:
directory: ./notarius-debug
pipelines:
dnd-npc-grounded:
input: seriatim
steps:
- id: identify-npcs
artifacts:
npcs:
extract:
module: dnd/npcs
retries: 2
normalize: dnd/npcs
- id: grounded-events
references:
npcs:
artifact:
step: identify-npcs
lane: npcs
artifacts:
spells:
extract: dnd/spells
normalize: dnd/spells
combat:
extract:
module: dnd/combat-turns
retries: 2
normalize: dnd/combat-turns

View File

@@ -1,34 +0,0 @@
version: 3
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: false
directory: ""
debug:
directory: ./notarius-debug
pipelines:
dnd-npc-interactions:
input: seriatim
steps:
- id: identify-npcs
artifacts:
npcs:
extract:
module: dnd/npcs
retries: 2
normalize: dnd/npcs
- id: extract-interactions
references:
npcs:
artifact:
step: identify-npcs
lane: npcs
artifacts:
interactions:
extract:
module: dnd/npc-interactions
retries: 2
normalize: dnd/npc-interactions

View File

@@ -1,21 +0,0 @@
version: 3
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: false
directory: ""
debug:
directory: ./notarius-debug
pipelines:
dnd-session:
input: seriatim
chunk: generic
artifacts:
npcs:
extract:
module: dnd/npcs
retries: 2
normalize: dnd/npcs

View File

@@ -1,23 +0,0 @@
version: 3
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: false
directory: ""
debug:
directory: ./notarius-debug
pipelines:
dnd-scene-chunk-map:
input: seriatim
chunk: dnd/scenes
output:
module: json
options:
include_chunk_map: true
artifacts:
spells:
extract: dnd/spells
normalize: dnd/spells

View File

@@ -1,20 +0,0 @@
version: 3
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: bypass
directory: ./notarius-cache/chunk-plans
checkpoints:
enabled: false
directory: ./notarius-cache/checkpoints
debug:
directory: ./notarius-debug
pipelines:
dnd-scene-descriptions:
input: seriatim
chunk: dnd/scenes
artifacts:
scene-descriptions:
extract: dnd/scene-descriptions
normalize: dnd/scene-descriptions

View File

@@ -1,39 +0,0 @@
version: 3
concurrency:
total_llm: 1
stage_workers:
extract: 1
output:
directory: ./notarius-output
cache:
chunk_plans:
directory: /var/cache/notarius/chunk-plans
mode: auto
checkpoints:
enabled: false
directory: /var/cache/notarius/checkpoints
debug:
directory: ./notarius-debug
pipelines:
dnd-session:
input: seriatim
references:
party: ./dnd-spells-roster.txt
glossary: ./dnd-spells-glossary.txt
chunk:
module: generic
options:
max_units: 50
artifacts:
spells:
extract:
module: dnd/spells
retries: 2
# Overlay behavior binds the same catalog independently at each stage.
references:
spell_catalog: ./dnd-spells-catalog.json
normalize:
module: dnd/spells
# Normalize-stage references are local and must be bound explicitly.
references:
spell_catalog: ./dnd-spells-catalog.json

View File

@@ -1,19 +0,0 @@
version: 3
output:
directory: ./notarius-output
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: false
directory: ""
debug:
directory: ./notarius-debug
pipelines:
dnd-session:
input: seriatim
artifacts:
spells:
extract: dnd/spells
# Base-only behavior: normalization uses the embedded SRD catalog.
normalize: dnd/spells

View File

@@ -15,8 +15,7 @@ import (
func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) { func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-combat-turns.config.yml") cfg := productionCombatContractConfig()
cfg := loadMaintainedExample(t, configPath)
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)}) effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
if err != nil { if err != nil {
t.Fatalf("Resolve() error = %v, want nil", err) t.Fatalf("Resolve() error = %v, want nil", err)
@@ -97,9 +96,8 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
func TestProductionCombatConfigurationRejectsLooseOptionsAndLaneValidators(t *testing.T) { func TestProductionCombatConfigurationRejectsLooseOptionsAndLaneValidators(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-combat-turns.config.yml")
resolve := func(mutate func(*pipeline.PipelineProfile)) error { resolve := func(mutate func(*pipeline.PipelineProfile)) error {
cfg := loadMaintainedExample(t, configPath) cfg := productionCombatContractConfig()
profile := cfg.Pipelines["dnd-combat"] profile := cfg.Pipelines["dnd-combat"]
mutate(&profile) mutate(&profile)
cfg.Pipelines["dnd-combat"] = profile cfg.Pipelines["dnd-combat"] = profile
@@ -131,7 +129,7 @@ func TestProductionCombatConfigurationRejectsLooseOptionsAndLaneValidators(t *te
func TestProductionCombatConfigurationResolvesTypedUnconditionalValidators(t *testing.T) { func TestProductionCombatConfigurationResolvesTypedUnconditionalValidators(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
cfg := loadMaintainedExample(t, repositoryPath("examples", "dnd-combat-turns.config.yml")) cfg := productionCombatContractConfig()
profile := cfg.Pipelines["dnd-combat"] profile := cfg.Pipelines["dnd-combat"]
lane := profile.Artifacts["combat"] lane := profile.Artifacts["combat"]
lane.Extract.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}} lane.Extract.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}}
@@ -150,6 +148,22 @@ func TestProductionCombatConfigurationResolvesTypedUnconditionalValidators(t *te
} }
} }
func productionCombatContractConfig() config.Config {
cfg := config.Default()
cfg.Pipelines["dnd-combat"] = pipeline.PipelineProfile{
ID: "dnd-combat",
Input: pipeline.Binding("seriatim"),
Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"combat": {
Extract: pipeline.ModuleBinding{Module: combatextract.Key, Retries: 2},
Normalize: pipeline.Binding(combatnormalize.Key),
},
},
}
return cfg
}
func hasReferenceSlot(slots []contracts.ReferenceSlot, name string) bool { func hasReferenceSlot(slots []contracts.ReferenceSlot, name string) bool {
for _, slot := range slots { for _, slot := range slots {
if slot.Name == name { if slot.Name == name {

View File

@@ -16,8 +16,7 @@ import (
func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) { func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
catalog := catalogFromRegistries(components.registries) catalog := catalogFromRegistries(components.registries)
configPath := repositoryPath("examples", "dnd-npcs.config.yml") cfg := productionNPCContractConfig()
cfg := loadMaintainedExample(t, configPath)
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalog}) effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalog})
if err != nil { if err != nil {
t.Fatalf("Resolve() error = %v, want nil", err) t.Fatalf("Resolve() error = %v, want nil", err)
@@ -81,9 +80,8 @@ func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *testing.T) { func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-npcs.config.yml")
resolve := func(mutate func(*pipeline.PipelineProfile)) error { resolve := func(mutate func(*pipeline.PipelineProfile)) error {
cfg := loadMaintainedExample(t, configPath) cfg := productionNPCContractConfig()
profile := cfg.Pipelines["dnd-session"] profile := cfg.Pipelines["dnd-session"]
mutate(&profile) mutate(&profile)
cfg.Pipelines["dnd-session"] = profile cfg.Pipelines["dnd-session"] = profile
@@ -123,6 +121,22 @@ func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *tes
} }
} }
func productionNPCContractConfig() config.Config {
cfg := config.Default()
cfg.Pipelines["dnd-session"] = pipeline.PipelineProfile{
ID: "dnd-session",
Input: pipeline.Binding("seriatim"),
Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"npcs": {
Extract: pipeline.ModuleBinding{Module: npcextract.Key, Retries: 2},
Normalize: pipeline.Binding(npcnormalize.Key),
},
},
}
return cfg
}
func validatorChain(resolved pipeline.ResolvedPipeline, stage pipeline.ModuleStage, module string) []pipeline.ModuleBinding { func validatorChain(resolved pipeline.ResolvedPipeline, stage pipeline.ModuleStage, module string) []pipeline.ModuleBinding {
for _, chain := range resolved.ValidatorChains { for _, chain := range resolved.ValidatorChains {
if chain.Stage == stage && chain.ModuleKey == module { if chain.Stage == stage && chain.ModuleKey == module {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"testing" "testing"
@@ -32,11 +33,14 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err) t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err)
} }
if example.name == "production" { if example.name == "complete" {
if len(materialized.Steps[0].ArtifactLanes) != 1 || if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:npcs,scene-descriptions|extract-events:combat-turns,npc-interactions,spells" {
len(materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 || t.Fatalf("complete example steps and lanes = %v, want every D&D extractor in the documented two-step composition", got)
len(materialized.Steps[0].ArtifactLanes[0].NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 { }
t.Fatalf("production spell catalog reference was not materialized: %#v", materialized.Steps[0].ArtifactLanes) spellLane := referenceContractLane(t, materialized, "spells")
if len(spellLane.ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 ||
len(spellLane.NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 {
t.Fatalf("complete example spell catalog reference was not materialized: %#v", spellLane)
} }
} }
} }
@@ -49,6 +53,36 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
} }
} }
func TestMaintainedConfigurationExampleSet(t *testing.T) {
entries, err := os.ReadDir(repositoryPath("examples"))
if err != nil {
t.Fatal(err)
}
var names []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".config.yml") {
names = append(names, entry.Name())
}
}
sort.Strings(names)
if got := strings.Join(names, ","); got != "dnd-complete.config.yml,dnd-minimal.config.yml" {
t.Fatalf("maintained configuration examples = %q, want only the minimal and complete D&D examples", got)
}
}
func exampleStepLaneIDs(resolved pipeline.ResolvedPipeline) []string {
result := make([]string, 0, len(resolved.Steps))
for _, step := range resolved.Steps {
laneIDs := make([]string, 0, len(step.ArtifactLanes))
for _, lane := range step.ArtifactLanes {
laneIDs = append(laneIDs, lane.ID)
}
sort.Strings(laneIDs)
result = append(result, step.ID+":"+strings.Join(laneIDs, ","))
}
return result
}
func TestMaintainedMinimalInvocationProducesJSONBundle(t *testing.T) { func TestMaintainedMinimalInvocationProducesJSONBundle(t *testing.T) {
outputRoot := filepath.Join(t.TempDir(), "output") outputRoot := filepath.Join(t.TempDir(), "output")
fake := &productionFakeLLMClient{} fake := &productionFakeLLMClient{}
@@ -56,7 +90,7 @@ func TestMaintainedMinimalInvocationProducesJSONBundle(t *testing.T) {
var stdout, stderr strings.Builder var stdout, stderr strings.Builder
code := RunWithOptions([]string{ code := RunWithOptions([]string{
"run", "dnd-session", "run", "dnd-session",
"--config", repositoryPath("examples", "dnd-spells.config.yml"), "--config", repositoryPath("examples", "dnd-minimal.config.yml"),
"--input", repositoryPath("examples", "seriatim-minimal-transcript.json"), "--input", repositoryPath("examples", "seriatim-minimal-transcript.json"),
"--only", "spells", "--chunk_cache", "bypass", "--output-dir", outputRoot, "--only", "spells", "--chunk_cache", "bypass", "--output-dir", outputRoot,
}, &stdout, &stderr, options) }, &stdout, &stderr, options)
@@ -130,7 +164,7 @@ func TestMaintainedMalformedInputOnlyRecordsDebugFailureWhenRequested(t *testing
options := productionRunOptions(t, &productionFakeLLMClient{}) options := productionRunOptions(t, &productionFakeLLMClient{})
args := []string{ args := []string{
"run", "dnd-session", "run", "dnd-session",
"--config", repositoryPath("examples", "dnd-spells.config.yml"), "--config", repositoryPath("examples", "dnd-minimal.config.yml"),
"--input", malformed, "--chunk_cache", "bypass", "--output-dir", outputRoot, "--input", malformed, "--chunk_cache", "bypass", "--output-dir", outputRoot,
} }
if debug { if debug {

View File

@@ -3,6 +3,7 @@ package cli
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
@@ -22,9 +23,24 @@ func TestOversizedNPCRegistryFailsBeforeRuntimeAndCheckpointConstruction(t *test
t.Fatal(err) t.Fatal(err)
} }
checkpointRoot := filepath.Join(t.TempDir(), "checkpoints") checkpointRoot := filepath.Join(t.TempDir(), "checkpoints")
content := string(readRepositoryFile(t, "examples", "dnd-spells.config.yml")) content := fmt.Sprintf(`version: 3
content = replaceRequiredOnce(t, content, " extract: dnd/spells", " extract:\n module: dnd/spells\n references:\n npcs: "+npcPath) cache:
content = replaceRequiredOnce(t, content, " enabled: false\n directory: \"\"", " enabled: true\n directory: "+checkpointRoot) chunk_plans:
mode: bypass
checkpoints:
enabled: true
directory: %q
pipelines:
dnd-session:
input: seriatim
artifacts:
spells:
extract:
module: dnd/spells
references:
npcs: %q
normalize: dnd/spells
`, checkpointRoot, npcPath)
configPath := filepath.Join(t.TempDir(), "config.yml") configPath := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)

View File

@@ -147,7 +147,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
func TestDefaultCLICompositionValidatesRepresentativeConfiguration(t *testing.T) { func TestDefaultCLICompositionValidatesRepresentativeConfiguration(t *testing.T) {
var stdout, stderr strings.Builder var stdout, stderr strings.Builder
code := RunWithOptions([]string{ code := RunWithOptions([]string{
"config", "validate", "--config", repositoryPath("examples", "dnd-spells.config.yml"), "--pipeline", "dnd-session", "config", "validate", "--config", repositoryPath("examples", "dnd-minimal.config.yml"), "--pipeline", "dnd-session",
}, &stdout, &stderr, Options{}) }, &stdout, &stderr, Options{})
if code != 0 || stderr.Len() != 0 { if code != 0 || stderr.Len() != 0 {
t.Fatalf("validate representative config with default composition: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String()) t.Fatalf("validate representative config with default composition: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
@@ -176,7 +176,7 @@ func TestProductionPromptAssetsPrepareWithoutProviderCredentials(t *testing.T) {
func TestProductionSpellValidatorsPrepareFromMaterializedCatalog(t *testing.T) { func TestProductionSpellValidatorsPrepareFromMaterializedCatalog(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml") configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session")) effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil { if err != nil {
t.Fatalf("resolve production spell configuration: %v", err) t.Fatalf("resolve production spell configuration: %v", err)
@@ -203,7 +203,7 @@ func TestProductionSpellValidatorsPrepareFromMaterializedCatalog(t *testing.T) {
func TestProductionSpellNormalizerRejectsInvalidCatalogReferencesBeforeExecution(t *testing.T) { func TestProductionSpellNormalizerRejectsInvalidCatalogReferencesBeforeExecution(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml") configPath := writeProductionSpellCatalogContractConfig(t)
resolve := func(t *testing.T) pipeline.ResolvedPipeline { resolve := func(t *testing.T) pipeline.ResolvedPipeline {
t.Helper() t.Helper()
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session")) effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
@@ -261,12 +261,15 @@ func TestProductionSpellNormalizerRejectsInvalidCatalogReferencesBeforeExecution
t.Fatal(err) t.Fatal(err)
} }
checkpointRoot := filepath.Join(t.TempDir(), "checkpoints") checkpointRoot := filepath.Join(t.TempDir(), "checkpoints")
content := string(readRepositoryFile(t, "examples", "dnd-spells-production.config.yml")) content := productionSpellCatalogContractConfig(t)
content = replaceRequiredOnce(t, content, "./dnd-spells-roster.txt", repositoryPath("examples", "dnd-spells-roster.txt")) catalogSource := repositoryPath("examples", "dnd-spell-catalog.json")
content = replaceRequiredOnce(t, content, "./dnd-spells-glossary.txt", repositoryPath("examples", "dnd-spells-glossary.txt")) if count := strings.Count(content, catalogSource); count != 2 {
content = strings.Replace(content, "./dnd-spells-catalog.json", repositoryPath("examples", "dnd-spells-catalog.json"), 1) t.Fatalf("spell catalog source occurs %d times, want extract and normalize bindings", count)
content = replaceRequiredOnce(t, content, "./dnd-spells-catalog.json", catalogPath) }
content = replaceRequiredOnce(t, content, " enabled: false\n directory: /var/cache/notarius/checkpoints", " enabled: true\n directory: "+checkpointRoot) content = strings.Replace(content, catalogSource, "__extract_catalog__", 1)
content = replaceRequiredOnce(t, content, catalogSource, catalogPath)
content = replaceRequiredOnce(t, content, "__extract_catalog__", catalogSource)
content = replaceRequiredOnce(t, content, " enabled: false\n directory: \"\"", " enabled: true\n directory: "+checkpointRoot)
configFile := filepath.Join(t.TempDir(), "config.yml") configFile := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(configFile, []byte(content), 0o600); err != nil { if err := os.WriteFile(configFile, []byte(content), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -382,7 +385,7 @@ func TestProductionLLMClientFactoriesRejectInvalidConstruction(t *testing.T) {
} }
func TestProductionConfigValidationCoversModuleAndVariantFailures(t *testing.T) { func TestProductionConfigValidationCoversModuleAndVariantFailures(t *testing.T) {
base := string(readRepositoryFile(t, "examples", "dnd-spells.config.yml")) base := string(readRepositoryFile(t, "examples", "dnd-minimal.config.yml"))
validPath := writeProductionContractConfig(t, base) validPath := writeProductionContractConfig(t, base)
options := productionCLIOptions(t) options := productionCLIOptions(t)
var stdout, stderr strings.Builder var stdout, stderr strings.Builder
@@ -438,7 +441,7 @@ func TestProductionConfigValidationCoversModuleAndVariantFailures(t *testing.T)
} }
func TestProductionNormalizeValidatorOverrideRemainsAuthoritative(t *testing.T) { func TestProductionNormalizeValidatorOverrideRemainsAuthoritative(t *testing.T) {
base := string(readRepositoryFile(t, "examples", "dnd-spells.config.yml")) base := string(readRepositoryFile(t, "examples", "dnd-minimal.config.yml"))
content := replaceRequiredOnce(t, base, " normalize: dnd/spells\n", " normalize:\n module: dnd/spells\n validators:\n - module: generic/always_accept\n - module: generic/valid_json\n") content := replaceRequiredOnce(t, base, " normalize: dnd/spells\n", " normalize:\n module: dnd/spells\n validators:\n - module: generic/always_accept\n - module: generic/valid_json\n")
path := writeProductionContractConfig(t, content) path := writeProductionContractConfig(t, content)
components := productionTestComponents(t) components := productionTestComponents(t)
@@ -531,17 +534,45 @@ type maintainedExample struct {
func maintainedExampleFiles(t *testing.T) []maintainedExample { func maintainedExampleFiles(t *testing.T) []maintainedExample {
t.Helper() t.Helper()
return []maintainedExample{ return []maintainedExample{
{name: "minimal", path: repositoryPath("examples", "dnd-spells.config.yml"), pipelineIDs: []string{"dnd-session"}}, {name: "minimal", path: repositoryPath("examples", "dnd-minimal.config.yml"), pipelineIDs: []string{"dnd-session"}},
{name: "production", path: repositoryPath("examples", "dnd-spells-production.config.yml"), pipelineIDs: []string{"dnd-session"}}, {name: "complete", path: repositoryPath("examples", "dnd-complete.config.yml"), pipelineIDs: []string{"dnd-session"}},
{name: "npcs", path: repositoryPath("examples", "dnd-npcs.config.yml"), pipelineIDs: []string{"dnd-session"}},
{name: "combat", path: repositoryPath("examples", "dnd-combat-turns.config.yml"), pipelineIDs: []string{"dnd-combat"}},
{name: "npc-grounded", path: repositoryPath("examples", "dnd-npc-grounded.config.yml"), pipelineIDs: []string{"dnd-npc-grounded"}},
{name: "npc-interactions", path: repositoryPath("examples", "dnd-npc-interactions.config.yml"), pipelineIDs: []string{"dnd-npc-interactions"}},
{name: "scene-descriptions", path: repositoryPath("examples", "dnd-scene-descriptions.config.yml"), pipelineIDs: []string{"dnd-scene-descriptions"}},
{name: "scene-chunk-map", path: repositoryPath("examples", "dnd-scene-chunk-map.config.yml"), pipelineIDs: []string{"dnd-scene-chunk-map"}},
} }
} }
func productionSpellCatalogContractConfig(t *testing.T) string {
t.Helper()
return fmt.Sprintf(`version: 3
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: false
directory: ""
pipelines:
dnd-session:
input: seriatim
references:
party: %q
glossary: %q
artifacts:
spells:
extract:
module: dnd/spells
retries: 2
references:
spell_catalog: %q
normalize:
module: dnd/spells
references:
spell_catalog: %q
`, repositoryPath("examples", "dnd-party.txt"), repositoryPath("examples", "dnd-glossary.txt"), repositoryPath("examples", "dnd-spell-catalog.json"), repositoryPath("examples", "dnd-spell-catalog.json"))
}
func writeProductionSpellCatalogContractConfig(t *testing.T) string {
t.Helper()
return writeProductionContractConfig(t, productionSpellCatalogContractConfig(t))
}
func loadMaintainedExample(t *testing.T, path string) config.Config { func loadMaintainedExample(t *testing.T, path string) config.Config {
t.Helper() t.Helper()
fileConfig, err := config.LoadFileConfig(path) fileConfig, err := config.LoadFileConfig(path)

View File

@@ -418,11 +418,13 @@ func (referenceContractCodecB) Decode([]byte) (stateTestArtifact, error) {
func referenceContractLane(t *testing.T, resolved pipeline.ResolvedPipeline, id string) pipeline.ResolvedArtifactLane { func referenceContractLane(t *testing.T, resolved pipeline.ResolvedPipeline, id string) pipeline.ResolvedArtifactLane {
t.Helper() t.Helper()
for _, lane := range resolved.Steps[0].ArtifactLanes { for _, step := range resolved.Steps {
for _, lane := range step.ArtifactLanes {
if lane.ID == id { if lane.ID == id {
return lane return lane
} }
} }
}
t.Fatalf("lane %q not found", id) t.Fatalf("lane %q not found", id)
return pipeline.ResolvedArtifactLane{} return pipeline.ResolvedArtifactLane{}
} }

View File

@@ -24,7 +24,7 @@ import (
func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testing.T) { func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml") configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session")) effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil { if err != nil {
t.Fatalf("resolve production configuration: %v", err) t.Fatalf("resolve production configuration: %v", err)
@@ -108,8 +108,8 @@ func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testin
} }
func TestConfiguredSpellCatalogBindingChangesResolvedPipelineIdentity(t *testing.T) { func TestConfiguredSpellCatalogBindingChangesResolvedPipelineIdentity(t *testing.T) {
base := string(readRepositoryFile(t, "examples", "dnd-spells-production.config.yml")) base := productionSpellCatalogContractConfig(t)
changed := strings.Replace(base, "./dnd-spells-catalog.json", "./alternate-spell-catalog.json", 1) changed := strings.Replace(base, repositoryPath("examples", "dnd-spell-catalog.json"), filepath.Join(t.TempDir(), "alternate-spell-catalog.json"), 1)
if changed == base { if changed == base {
t.Fatal("production configuration did not contain the maintained catalog binding") t.Fatal("production configuration did not contain the maintained catalog binding")
} }
@@ -138,7 +138,7 @@ func TestConfiguredSpellCatalogBindingChangesResolvedPipelineIdentity(t *testing
func TestSemanticSpellCatalogFingerprintChangesCheckpointIdentityWithoutReferenceChange(t *testing.T) { func TestSemanticSpellCatalogFingerprintChangesCheckpointIdentityWithoutReferenceChange(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml") configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session")) effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -200,7 +200,7 @@ func TestSemanticSpellCatalogFingerprintChangesCheckpointIdentityWithoutReferenc
func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t *testing.T) { func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml") configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session")) effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -328,7 +328,7 @@ func TestMaintainedProductionOverlayRunAlignsGroundingValidationAndProvenance(t
var stdout, stderr strings.Builder var stdout, stderr strings.Builder
code := RunWithOptions([]string{ code := RunWithOptions([]string{
"run", "dnd-session", "run", "dnd-session",
"--config", repositoryPath("examples", "dnd-spells-production.config.yml"), "--config", writeProductionSpellCatalogContractConfig(t),
"--input", repositoryPath("examples", "seriatim-minimal-transcript.json"), "--input", repositoryPath("examples", "seriatim-minimal-transcript.json"),
"--only", "spells", "--chunk_cache", "bypass", "--output-dir", outputRoot, "--only", "spells", "--chunk_cache", "bypass", "--output-dir", outputRoot,
}, &stdout, &stderr, options) }, &stdout, &stderr, options)
@@ -373,12 +373,12 @@ func TestMaintainedProductionOverlayRunAlignsGroundingValidationAndProvenance(t
if len(catalogProvenances) != 2 { if len(catalogProvenances) != 2 {
t.Fatalf("manifest references = %#v, want independently materialized extract and normalize catalog provenance", manifest.References) t.Fatalf("manifest references = %#v, want independently materialized extract and normalize catalog provenance", manifest.References)
} }
overlayBytes := readRepositoryFile(t, "examples", "dnd-spells-catalog.json") overlayBytes := readRepositoryFile(t, "examples", "dnd-spell-catalog.json")
for _, catalogProvenance := range catalogProvenances { for _, catalogProvenance := range catalogProvenances {
if catalogProvenance.Stage != "extract" && catalogProvenance.Stage != "normalize" { if catalogProvenance.Stage != "extract" && catalogProvenance.Stage != "normalize" {
t.Fatalf("catalog provenance = %#v, want extract or normalize scope", catalogProvenance) t.Fatalf("catalog provenance = %#v, want extract or normalize scope", catalogProvenance)
} }
if catalogProvenance.LaneID != "spells" || catalogProvenance.OriginType != "file" || catalogProvenance.MediaType != "application/json" || catalogProvenance.SizeBytes != int64(len(overlayBytes)) || catalogProvenance.Digest != digestBytes(overlayBytes) || !strings.Contains(catalogProvenance.OriginURI, "dnd-spells-catalog.json") { if catalogProvenance.LaneID != "spells" || catalogProvenance.OriginType != "file" || catalogProvenance.MediaType != "application/json" || catalogProvenance.SizeBytes != int64(len(overlayBytes)) || catalogProvenance.Digest != digestBytes(overlayBytes) || !strings.Contains(catalogProvenance.OriginURI, "dnd-spell-catalog.json") {
t.Fatalf("catalog provenance = %#v, want raw overlay provenance in both scopes", catalogProvenance) t.Fatalf("catalog provenance = %#v, want raw overlay provenance in both scopes", catalogProvenance)
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"path/filepath"
"sync" "sync"
"testing" "testing"
@@ -50,7 +51,7 @@ func TestProductionSpellCatalogValidationRetries(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
components := productionTestComponents(t) components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml") configPath := writeProductionSpellCatalogContractConfig(t)
cfg := loadMaintainedExample(t, configPath) cfg := loadMaintainedExample(t, configPath)
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalogFromRegistries(components.registries)}) effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalogFromRegistries(components.registries)})
if err != nil { if err != nil {
@@ -58,7 +59,7 @@ func TestProductionSpellCatalogValidationRetries(t *testing.T) {
} }
materialized, _, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, catalogFromRegistries(components.registries), pipeline.ReferenceMaterializationOptions{ materialized, _, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, catalogFromRegistries(components.registries), pipeline.ReferenceMaterializationOptions{
ConfigPath: configPath, ConfigPath: configPath,
WorkingDir: repositoryPath("examples"), WorkingDir: filepath.Dir(configPath),
}) })
if err != nil { if err != nil {
t.Fatalf("materialize production references: %v", err) t.Fatalf("materialize production references: %v", err)