Remove legacy party configuration

This commit is contained in:
2026-09-02 12:04:35 +00:00
parent 61095e69ce
commit 82d75e7beb
35 changed files with 110 additions and 364 deletions

View File

@@ -106,10 +106,10 @@ family declarations, and runtime provenance.
`config sources` reports the same fully validated resolution without printing
effective values. Its header identifies the root, ordered imports, selected
profile and overlay, selected campaign, party mode/source, and digest. The
profile and overlay, selected campaign, party source, and digest. The
remaining tab-separated records are sorted as `path`, `role`, and `source`.
Roles distinguish root, import, profile, centralized default, campaign, party,
legacy-player, and generated family ownership. A generated party member has
and generated family ownership. A generated party member has
one family record and one party record at the same logical path. The output
never reads or prints secret values.

View File

@@ -79,8 +79,8 @@ Use the [production/testing bundle](../examples/production-testing/pipeline.yml)
as the complete copyable migration reference. Split stable pipeline settings
into explicit additive imports, place production/testing differences in one
selected overlay, and retain a production `default_profile`. Convert campaign
rosters to [canonical party input](integrations/party.md), remove a separate
`players_file`, then express character work as families. Inspect the result
rosters to [canonical party input](integrations/party.md), then express
character work as families. Inspect the result
with `config validate`, `config show`, and `config sources`; use `config diff`
to review profiles before running a session. Unversioned party documents are
rejected.
@@ -112,13 +112,13 @@ remote state with an unsafe legacy identity must be migrated before use.
- Pipeline defaults are applied before validation.
- Campaign and session identities must agree.
- Required stable files (`speakers_file`, `autocorrect_file`, `glossary_file`,
`party_file`) and the optional `spell_catalog_file` resolve from session
overrides when provided, otherwise from campaign defaults. An empty or
omitted session spell-catalog value inherits the campaign value.
`party_file`) are campaign-owned; the first three may be overridden by the
session. The optional `spell_catalog_file` also inherits a campaign value
when omitted by the session.
- `party_file` is parsed when pipeline and campaign configuration are combined.
It must be a versioned [canonical party](integrations/party.md), is
campaign-owned, derives the players input internally, and forbids both a
separate `players_file` and a session `party_file` override.
campaign-owned and derives the players input internally. `players_file` and
session `party_file` are unsupported configuration keys.
- Exactly one audio mode must be configured in session input:
- local (`audio_dir` or `audio_files`), or
- S3 (`audio_s3.prefix`).
@@ -521,7 +521,6 @@ integration.
| `inputs.speakers_file` | string | Yes | stable input default |
| `inputs.autocorrect_file` | string | Yes | stable input default |
| `inputs.glossary_file` | string | Yes | stable input default |
| `inputs.players_file` | string | No | retained temporarily for configuration compatibility; forbidden with the required canonical party |
| `inputs.party_file` | string | Yes | stable campaign party source; relative paths resolve from `campaign.yml` |
| `inputs.spell_catalog_file` | string | No | optional spell-catalog overlay default; required when a Notarius reference selects `narratio.input.spell_catalog` |
@@ -537,8 +536,6 @@ integration.
| `inputs.speakers_file` | string | No | overrides campaign stable input |
| `inputs.autocorrect_file` | string | No | overrides campaign stable input |
| `inputs.glossary_file` | string | No | overrides campaign stable input |
| `inputs.players_file` | string | No | retained temporarily for configuration compatibility; forbidden with the required canonical party |
| `inputs.party_file` | string | No | retained temporarily for configuration compatibility; forbidden for a canonical campaign party |
| `inputs.spell_catalog_file` | string | No | overrides the optional campaign spell catalog; empty or omitted inherits the campaign value |
| `inputs.audio_dir` | string | Conditional | local audio mode |
| `inputs.audio_files[]` | list[string] | Conditional | local audio mode |

View File

@@ -41,10 +41,7 @@ The document has exactly one YAML document and accepts no unknown fields.
Unversioned party inputs are rejected. Replace each legacy party document with
the versioned structure above, move each roster entry under `characters`, and
remove separate player and session-party overrides from the selected campaign
and session configuration. The configuration-key removal itself is staged
separately; current configurations must already omit those competing values
when using a canonical party.
remove separate player and session-party settings: those keys are unsupported.
The configuration rules and source-relative path behavior are defined in the
[Configuration Reference](../config.md).

View File

@@ -92,8 +92,8 @@ with no projected difference is treated as an internal consistency error.
Campaign context construction reads the campaign-owned party source through
`ParseParty`. The required canonical party retains its raw bytes and normalized
roster in runtime-only `ResolvedParty` provenance. Canonical resolution creates
a virtual `derived_from_party` players input and rejects competing campaign or
session players files and session party overrides.
a deterministic `derived_from_party` players projection. The party is the only
configured roster authority; player and session-party fields are unsupported.
## Canonical Party Domain

View File

@@ -185,7 +185,7 @@ fields and mode shell that supported them.
## Stage 3 — Remove Legacy Party And Players Configuration
**Status: Pending**
**Status: Completed**
### Goal

View File

@@ -92,8 +92,8 @@ Safe fix:
- replace the file with one exact, single-document
[`narratio.party.v1` party](./integrations/party.md); do not add a version
marker to an otherwise incompatible legacy structure. Remove competing
player and session-party settings while migrating.
marker to an otherwise incompatible legacy structure. The versioned party is
the only configured roster authority.
Relevant reference: [Canonical Party Input](./integrations/party.md).

View File

@@ -53,7 +53,7 @@ with the sample campaign and a compatible local- or S3-audio session.
[party](campaigns/sample-campaign/party.yml) fixture, plus an optional
[spell-catalog overlay](campaigns/sample-campaign/spell_catalog.json) that
follows the Notarius v0.6 contract. Narratio derives the players projection
from this party source; the campaign deliberately has no `players_file`.
from this party source.
- [Sample speaker audio](audio/sample-speaker.flac) is a text placeholder that
reserves the expected filename and directory shape. Replace it with a real
FLAC file before running transcription.

View File

@@ -274,7 +274,7 @@ func writeInspectionSourceHeader(out io.Writer, resolved *inspectionConfig) erro
if _, err := fmt.Fprintf(out, "campaign: id=%s source=%s\n", config.CampaignID(resolved.Campaign), config.NormalizedConfigurationPath(resolved.CampaignPath)); err != nil {
return err
}
if _, err := fmt.Fprintf(out, "party: mode=%s source=%s\n", resolved.Party.Mode, config.NormalizedConfigurationPath(resolved.Party.Source.Path)); err != nil {
if _, err := fmt.Fprintf(out, "party: source=%s\n", config.NormalizedConfigurationPath(resolved.Party.Source.Path)); err != nil {
return err
}
}

View File

@@ -400,7 +400,7 @@ func TestConfigSourcesReportsStableOwnership(t *testing.T) {
"imports: none",
"profile: name=testing selection=cli overlay=",
"campaign: id=sample-campaign source=",
"party: mode=canonical source=",
"party: source=",
"digest: ",
"path\trole\tsource\n",
"workspace.root\troot\t",
@@ -423,7 +423,7 @@ func TestConfigSourcesReportsCanonicalFamilyAndPublishOrigins(t *testing.T) {
output := out.String()
partyPath := filepath.Join(filepath.Dir(campaignPath), "party.yml")
for _, want := range []string{
"party: mode=canonical source=" + partyPath,
"party: source=" + partyPath,
"derived.players\tparty\t" + partyPath,
"scriptorium.artifacts.character_note_arannis.enabled\tfamily\t" + pipelinePath,
"scriptorium.artifacts.character_note_arannis.enabled\tparty\t" + partyPath,

View File

@@ -233,7 +233,7 @@ characters:
if err != nil {
t.Fatalf("ParseParty(changed party) error = %v", err)
}
cfg.Party.Canonical = partyDocument.Canonical
cfg.Party.Canonical = partyDocument
prepared := loadLifecycleManifest(t, cfg)
prepare, err := stage.Select("prepare")
@@ -290,7 +290,7 @@ characters:
if err != nil {
t.Fatalf("ParseParty(canonical party) error = %v", err)
}
cfg.Party.Canonical = partyDocument.Canonical
cfg.Party.Canonical = partyDocument
if _, err := executeStages(context.Background(), cfg, prepareExtractLifecyclePlan(t), RunOptions{Env: env}); err != nil {
t.Fatalf("executeStages() error = %v", err)

View File

@@ -56,17 +56,18 @@ func inspectStableInputs(cfg *config.Config) []stableInputCheck {
name string
in config.ResolvedInputFile
optional bool
derived bool
}{
{name: "speakers", in: cfg.StableInputs.SpeakersFile},
{name: "autocorrect", in: cfg.StableInputs.AutocorrectFile},
{name: "glossary", in: cfg.StableInputs.GlossaryFile},
{name: "players", in: cfg.StableInputs.PlayersFile},
{name: "players", derived: true},
{name: "party", in: cfg.StableInputs.PartyFile},
{name: "spell_catalog", in: cfg.StableInputs.SpellCatalogFile, optional: true},
}
out := make([]stableInputCheck, 0, len(items))
for _, item := range items {
if item.name == "players" && item.in.Source == "derived_from_party" {
if item.derived {
out = append(out, stableInputCheck{Name: item.name, Path: "derived from canonical party"})
continue
}

View File

@@ -82,8 +82,8 @@ inputs:
t.Fatalf("loadCommandConfig() error = %v", err)
}
defer func() { _ = loaded.Close() }()
if loaded.Config.Party.Mode != config.PartyModeCanonical || loaded.Config.StableInputs.PlayersFile.Source != "derived_from_party" {
t.Fatalf("remote config party = %#v, players = %#v", loaded.Config.Party, loaded.Config.StableInputs.PlayersFile)
if loaded.Config.Party.Canonical == nil {
t.Fatalf("remote config party = %#v", loaded.Config.Party)
}
}

View File

@@ -1335,8 +1335,7 @@ func testConfig(t *testing.T) *config.Config {
CampaignPath: campaignPath,
SessionPath: sessionPath,
Party: config.ResolvedParty{
Mode: config.PartyModeCanonical,
Canonical: partyDocument.Canonical,
Canonical: partyDocument,
Source: config.PartySource{
Path: filepath.Join(cfgDir, "party.yml"),
ConfigPath: campaignPath,
@@ -1359,7 +1358,6 @@ func testConfig(t *testing.T) *config.Config {
ConfigPath: campaignPath,
Source: "campaign_config",
},
PlayersFile: config.ResolvedInputFile{Source: "derived_from_party"},
PartyFile: config.ResolvedInputFile{
Path: "./party.yml",
ConfigPath: campaignPath,

View File

@@ -40,7 +40,7 @@ func expandPipelineArtifactFamilies(cfg *PipelineConfig, party ResolvedParty) er
if len(families) == 0 {
return nil
}
if party.Mode != PartyModeCanonical || party.Canonical == nil {
if party.Canonical == nil {
return fmt.Errorf("pipeline.scriptorium.artifact_families requires a canonical campaign party")
}

View File

@@ -183,7 +183,7 @@ func TestArtifactFamiliesRejectGeneratedKeyAndOutputCollisions(t *testing.T) {
Scriptorium: &ScriptoriumConfig{Artifacts: test.artifacts},
resolution: &pipelineResolutionMetadata{artifactFamilies: map[string]ScriptoriumArtifactFamilyConfig{"character_meta": base}},
}
err := expandPipelineArtifactFamilies(pipeline, ResolvedParty{Mode: PartyModeCanonical, Canonical: document.Canonical})
err := expandPipelineArtifactFamilies(pipeline, ResolvedParty{Canonical: document})
if err == nil || !strings.Contains(err.Error(), test.wantError) {
t.Fatalf("expandPipelineArtifactFamilies() error = %v, want %q", err, test.wantError)
}
@@ -290,12 +290,12 @@ func TestArtifactFamiliesRejectInvalidMemberDependenciesAndSources(t *testing.T)
if err != nil {
t.Fatal(err)
}
if err := expandPipelineArtifactFamilies(pipeline, ResolvedParty{Mode: PartyModeCanonical, Canonical: document.Canonical}); err == nil || !strings.Contains(err.Error(), "outside an artifact family") {
if err := expandPipelineArtifactFamilies(pipeline, ResolvedParty{Canonical: document}); err == nil || !strings.Contains(err.Error(), "outside an artifact family") {
t.Fatalf("concrete member source error = %v", err)
}
pipeline.Scriptorium.Artifacts["explicit"] = ScriptoriumArtifactConfig{Inputs: map[string]ScriptoriumInputConfig{"bad": {Source: "narratio.member_artifact."}}}
pipeline.resolution.artifactFamiliesExpanded = false
if err := expandPipelineArtifactFamilies(pipeline, ResolvedParty{Mode: PartyModeCanonical, Canonical: document.Canonical}); err == nil || !strings.Contains(err.Error(), "malformed member artifact source") {
if err := expandPipelineArtifactFamilies(pipeline, ResolvedParty{Canonical: document}); err == nil || !strings.Contains(err.Error(), "malformed member artifact source") {
t.Fatalf("malformed concrete member source error = %v", err)
}
}

View File

@@ -114,7 +114,6 @@ func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./campaign-speakers.yml", campaignPath, "campaign_config")
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "", "", "derived_from_party")
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./campaign-party.yml", campaignPath, "campaign_config")
}
@@ -134,7 +133,6 @@ func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./session-speakers.yml", sessionPath, "session_config")
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "", "", "derived_from_party")
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./campaign-party.yml", campaignPath, "campaign_config")
}

View File

@@ -64,7 +64,6 @@ type CampaignInputsConfig struct {
SpeakersFile string `yaml:"speakers_file"`
AutocorrectFile string `yaml:"autocorrect_file"`
GlossaryFile string `yaml:"glossary_file"`
PlayersFile string `yaml:"players_file"`
PartyFile string `yaml:"party_file"`
SpellCatalogFile string `yaml:"spell_catalog_file"`
}
@@ -357,8 +356,6 @@ type SessionInputsConfig struct {
SpeakersFile string `yaml:"speakers_file"`
AutocorrectFile string `yaml:"autocorrect_file"`
GlossaryFile string `yaml:"glossary_file"`
PlayersFile string `yaml:"players_file"`
PartyFile string `yaml:"party_file"`
SpellCatalogFile string `yaml:"spell_catalog_file"`
}
@@ -373,7 +370,6 @@ type ResolvedStableInputs struct {
SpeakersFile ResolvedInputFile
AutocorrectFile ResolvedInputFile
GlossaryFile ResolvedInputFile
PlayersFile ResolvedInputFile
PartyFile ResolvedInputFile
SpellCatalogFile ResolvedInputFile
}
@@ -385,11 +381,10 @@ type ResolvedInputFile struct {
Source string
}
// ResolvedParty records the selected party mode, its non-secret source
// provenance, and canonical domain data when available. It is runtime-only and
// must never be copied into manifests as raw party content.
// ResolvedParty records the campaign-owned canonical party and its non-secret
// source provenance. It is runtime-only and must never be copied into manifests
// as raw party content.
type ResolvedParty struct {
Mode PartyMode
Source PartySource
Canonical *CanonicalParty
}

View File

@@ -126,9 +126,8 @@ func EffectivePipelineSources(cfg *PipelineConfig, party ResolvedParty) ([]Effec
}
// EffectiveCampaignSources projects selected campaign and party ownership
// without retaining or rendering campaign values. Canonical players are a
// derived party value, while a legacy players file remains explicitly marked
// as legacy input provenance.
// without retaining or rendering campaign values. Players are derived from the
// canonical party source.
func EffectiveCampaignSources(campaignPath string, campaign *CampaignConfig, party ResolvedParty) []EffectivePipelineSourceRecord {
if campaign == nil {
return nil
@@ -143,21 +142,11 @@ func EffectiveCampaignSources(campaignPath string, campaign *CampaignConfig, par
if strings.TrimSpace(campaign.Inputs.SpellCatalogFile) != "" {
records = append(records, EffectivePipelineSourceRecord{Path: "campaign.inputs.spell_catalog_file", Role: "campaign", Source: campaignSource})
}
if party.Mode == PartyModeCanonical {
partySource := normalizedProvenancePath(party.Source.Path)
records = append(records,
EffectivePipelineSourceRecord{Path: "campaign.inputs.party_file", Role: "party", Source: partySource},
EffectivePipelineSourceRecord{Path: "derived.players", Role: "party", Source: partySource},
)
} else {
partySource := normalizedProvenancePath(party.Source.Path)
records = append(records, EffectivePipelineSourceRecord{Path: "campaign.inputs.party_file", Role: "party", Source: partySource})
if strings.TrimSpace(campaign.Inputs.PlayersFile) != "" {
records = append(records, EffectivePipelineSourceRecord{
Path: "campaign.inputs.players_file", Role: "legacy_player", Source: campaignInputSourcePath(campaignPath, campaign.Inputs.PlayersFile),
})
}
}
partySource := normalizedProvenancePath(party.Source.Path)
records = append(records,
EffectivePipelineSourceRecord{Path: "campaign.inputs.party_file", Role: "party", Source: partySource},
EffectivePipelineSourceRecord{Path: "derived.players", Role: "party", Source: partySource},
)
sort.Slice(records, func(left, right int) bool {
if records[left].Path != records[right].Path {
return records[left].Path < records[right].Path

View File

@@ -10,7 +10,7 @@ func TestIdentityValidationRejectsUnsafeSegments(t *testing.T) {
for _, value := range unsafe {
t.Run("campaign/"+strings.ReplaceAll(value, "/", "_"), func(t *testing.T) {
cfg := &CampaignConfig{CampaignID: value, Inputs: CampaignInputsConfig{
SpeakersFile: "speakers.yml", AutocorrectFile: "autocorrect.yml", GlossaryFile: "glossary.yml", PlayersFile: "players.yml", PartyFile: "party.yml",
SpeakersFile: "speakers.yml", AutocorrectFile: "autocorrect.yml", GlossaryFile: "glossary.yml", PartyFile: "party.yml",
}}
if err := validateCampaign(cfg); err == nil {
t.Fatalf("validateCampaign(%q) error = nil, want rejection", value)

View File

@@ -278,14 +278,9 @@ func LoadPipelineCampaignWithParty(pipelinePath string, pipeline *PipelineConfig
if campaign == nil {
return LoadedPipelineCampaign{}, fmt.Errorf("campaign config is required")
}
if party.Mode == "" {
if party.Canonical == nil {
return LoadedPipelineCampaign{}, fmt.Errorf("resolved campaign party is required")
}
if party.Mode == PartyModeCanonical {
if err := validateCanonicalPartySelection(campaign, nil); err != nil {
return LoadedPipelineCampaign{}, err
}
}
if err := expandPipelineArtifactFamilies(pipeline, party); err != nil {
return LoadedPipelineCampaign{}, err
}
@@ -322,18 +317,13 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
if loaded.Campaign == nil {
return nil, fmt.Errorf("campaign config is required")
}
if loaded.Party.Mode == "" {
if loaded.Party.Canonical == nil {
party, err := resolveCampaignParty(loaded.CampaignPath, loaded.Campaign)
if err != nil {
return nil, err
}
loaded.Party = party
}
if loaded.Party.Mode == PartyModeCanonical {
if err := validateCanonicalPartySelection(loaded.Campaign, nil); err != nil {
return nil, err
}
}
cfg := &Config{
Pipeline: loaded.Pipeline,
Campaign: loaded.Campaign,
@@ -344,11 +334,6 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
SessionSource: sessionSource,
Party: loaded.Party,
}
if cfg.Party.Mode == PartyModeCanonical && sessionCfg != nil {
if err := validateCanonicalPartySelection(loaded.Campaign, sessionCfg); err != nil {
return nil, err
}
}
if sessionCfg == nil {
return cfg, nil
}
@@ -363,22 +348,6 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
if strings.TrimSpace(cfg.SessionSource.LocalPath) == "" {
cfg.SessionSource.LocalPath = sessionPath
}
if cfg.Party.Mode == PartyModeCanonical {
stableInputs.PlayersFile = virtualPlayersInput()
cfg.Session.Inputs.PlayersFile = ""
} else {
party, err := resolvePartyInput(stableInputs.PartyFile)
if err != nil {
return nil, err
}
if party.Mode == PartyModeCanonical {
return nil, fmt.Errorf("session.inputs.party_file cannot select a canonical party; canonical parties are campaign-owned")
}
cfg.Party = party
if strings.TrimSpace(stableInputs.PlayersFile.Path) == "" {
return nil, fmt.Errorf("players_file is required with a legacy party")
}
}
cfg.StableInputs = stableInputs
return cfg, nil
}
@@ -456,15 +425,9 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
campaignPath,
sessionPath,
),
PlayersFile: selectStableInput(
campaignCfg.Inputs.PlayersFile,
sessionCfg.Inputs.PlayersFile,
campaignPath,
sessionPath,
),
PartyFile: selectStableInput(
campaignCfg.Inputs.PartyFile,
sessionCfg.Inputs.PartyFile,
"",
campaignPath,
sessionPath,
),
@@ -479,8 +442,6 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
sessionCfg.Inputs.AutocorrectFile = stable.AutocorrectFile.Path
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.Path
sessionCfg.Inputs.PlayersFile = stable.PlayersFile.Path
sessionCfg.Inputs.PartyFile = stable.PartyFile.Path
sessionCfg.Inputs.SpellCatalogFile = stable.SpellCatalogFile.Path
return stable, nil
}

View File

@@ -209,7 +209,7 @@ func TestExamplesLoadAndValidate(t *testing.T) {
if got := cfg.Pipeline.Scriptorium.Artifacts["character_items_arannis"].Inputs["character_meta"].Source; got != "narratio.artifact.character_meta_arannis" {
t.Fatalf("same-member input source = %q", got)
}
if cfg.Party.Mode != PartyModeCanonical || cfg.Party.Canonical == nil || len(cfg.Party.Canonical.Characters) != 2 {
if cfg.Party.Canonical == nil || len(cfg.Party.Canonical.Characters) != 2 {
t.Fatalf("canonical party = %#v", cfg.Party)
}
characters := cfg.Party.Canonical.Characters

View File

@@ -20,35 +20,6 @@ const (
PlayersSchemaVersion = "narratio.players.v1"
)
// PartyMode records whether a source document uses the canonical party
// contract or the bounded compatibility path for older opaque files.
type PartyMode string
const (
PartyModeCanonical PartyMode = "canonical"
PartyModeLegacy PartyMode = "legacy"
)
// PartyDocument classifies a party input. Canonical is populated only for a
// versioned canonical document; legacy contents intentionally remain opaque.
type PartyDocument struct {
Mode PartyMode
Canonical *CanonicalParty
}
// IsCanonical reports whether the document has validated canonical data.
func (d *PartyDocument) IsCanonical() bool {
return d != nil && d.Mode == PartyModeCanonical && d.Canonical != nil
}
// PlayersYAML renders the derived players projection for a canonical document.
func (d *PartyDocument) PlayersYAML() ([]byte, error) {
if !d.IsCanonical() {
return nil, fmt.Errorf("canonical party is required")
}
return d.Canonical.PlayersYAML()
}
// CanonicalParty is the normalized domain value for narratio.party.v1. Raw is
// retained independently so later preparation can copy the authored party file
// byte-for-byte rather than serializing normalized values.
@@ -84,7 +55,7 @@ type PartyClass struct {
}
// ParseParty strictly parses one supported party document.
func ParseParty(data []byte) (*PartyDocument, error) {
func ParseParty(data []byte) (*CanonicalParty, error) {
root, decoder, err := parsePartyFirstDocument(data)
if err != nil {
return nil, err
@@ -96,7 +67,7 @@ func ParseParty(data []byte) (*PartyDocument, error) {
if err != nil {
return nil, err
}
return &PartyDocument{Mode: PartyModeCanonical, Canonical: party}, nil
return party, nil
}
func parsePartyFirstDocument(data []byte) (*yaml.Node, *yaml.Decoder, error) {

View File

@@ -28,18 +28,17 @@ func resolvePartyInput(input ResolvedInputFile) (ResolvedParty, error) {
if err != nil {
return ResolvedParty{}, fmt.Errorf("read party input %q: %w", path, err)
}
document, err := ParseParty(raw)
party, err := ParseParty(raw)
if err != nil {
return ResolvedParty{}, fmt.Errorf("parse party input %q: %w", path, err)
}
return ResolvedParty{
Mode: PartyModeCanonical,
Source: PartySource{
Path: path,
ConfigPath: configPath,
Source: input.Source,
},
Canonical: document.Canonical,
Canonical: party,
}, nil
}
@@ -64,26 +63,3 @@ func resolveCampaignParty(campaignPath string, campaign *CampaignConfig) (Resolv
}
return resolvePartyInput(input)
}
func validateCanonicalPartySelection(campaign *CampaignConfig, session *SessionConfig) error {
if campaign == nil {
return fmt.Errorf("campaign config is required")
}
if strings.TrimSpace(campaign.Inputs.PlayersFile) != "" {
return fmt.Errorf("campaign.inputs.players_file is not allowed with a canonical party")
}
if session == nil {
return nil
}
if strings.TrimSpace(session.Inputs.PlayersFile) != "" {
return fmt.Errorf("session.inputs.players_file is not allowed with a canonical party")
}
if strings.TrimSpace(session.Inputs.PartyFile) != "" {
return fmt.Errorf("session.inputs.party_file cannot override a canonical campaign party")
}
return nil
}
func virtualPlayersInput() ResolvedInputFile {
return ResolvedInputFile{Source: "derived_from_party"}
}

View File

@@ -44,7 +44,7 @@ inputs:
if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err)
}
if cfg.Party.Mode != PartyModeCanonical || cfg.Party.Canonical == nil {
if cfg.Party.Canonical == nil {
t.Fatalf("party = %#v, want canonical party", cfg.Party)
}
if got, want := cfg.Party.Source.Path, partyPath; got != want {
@@ -56,9 +56,6 @@ inputs:
if !bytes.Equal(cfg.Party.Canonical.Raw, partyYAML) {
t.Fatal("canonical party bytes were not retained")
}
if got := cfg.StableInputs.PlayersFile; got.Source != "derived_from_party" || got.Path != "" || got.ConfigPath != "" {
t.Fatalf("derived players input = %#v, want virtual party projection source", got)
}
}
func TestCanonicalPartyRejectsSeparatePlayersAndSessionPartyOverrides(t *testing.T) {
@@ -68,9 +65,9 @@ func TestCanonicalPartyRejectsSeparatePlayersAndSessionPartyOverrides(t *testing
sessionExtra string
want string
}{
{name: "campaign players", campaignExtra: " players_file: players.yml\n", want: "campaign.inputs.players_file is not allowed"},
{name: "session players", sessionExtra: " players_file: players.yml\n", want: "session.inputs.players_file is not allowed"},
{name: "session party", sessionExtra: " party_file: party-override.yml\n", want: "session.inputs.party_file cannot override"},
{name: "campaign players", campaignExtra: " players_file: players.yml\n", want: "field players_file not found"},
{name: "session players", sessionExtra: " players_file: players.yml\n", want: "field players_file not found"},
{name: "session party", sessionExtra: " party_file: party-override.yml\n", want: "field party_file not found"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -126,15 +123,15 @@ func TestResolveAndLoadedCampaignShareCanonicalPartyResolution(t *testing.T) {
t.Fatalf("LoadPipelineCampaign() error = %v", err)
}
partial, err := ResolveLoadedPipelineCampaign(loaded, "", nil, SessionSource{})
if err != nil || partial.Party.Mode != PartyModeCanonical {
if err != nil || partial.Party.Canonical == nil {
t.Fatalf("ResolveLoadedPipelineCampaign() = %#v, %v; want canonical partial config", partial, err)
}
direct, err := Resolve(pipelinePath, pipeline, campaignPath, campaign, sessionPath, session, SessionSource{Source: "session_config", LocalPath: sessionPath})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
if direct.Party.Mode != PartyModeCanonical || direct.StableInputs.PlayersFile.Source != "derived_from_party" {
t.Fatalf("Resolve() party = %#v, players = %#v", direct.Party, direct.StableInputs.PlayersFile)
if direct.Party.Canonical == nil {
t.Fatalf("Resolve() party = %#v", direct.Party)
}
}

View File

@@ -27,13 +27,10 @@ characters:
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
if !document.IsCanonical() || document.Mode != PartyModeCanonical {
t.Fatalf("document mode = %#v, want canonical", document)
}
if !bytes.Equal(document.Canonical.Raw, data) {
if !bytes.Equal(document.Raw, data) {
t.Fatal("canonical raw bytes were not preserved")
}
characters := document.Canonical.Characters
characters := document.Characters
if len(characters) != 1 || characters[0].ID != "arannis" {
t.Fatalf("characters = %#v, want ordered arannis character", characters)
}
@@ -190,7 +187,7 @@ characters:
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
if got := len(document.Canonical.Characters); got != 2 {
if got := len(document.Characters); got != 2 {
t.Fatalf("character count = %d, want 2", got)
}
}
@@ -249,7 +246,7 @@ characters:
if err != nil {
t.Fatalf("PlayersYAML() error = %v", err)
}
again, err := document.Canonical.PlayersYAML()
again, err := document.PlayersYAML()
if err != nil {
t.Fatalf("PlayersYAML() repeat error = %v", err)
}

View File

@@ -17,7 +17,6 @@ inputs:
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
party_file: ./party.yml
`
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
t.Fatalf("write session.yml: %v", err)
@@ -41,7 +40,6 @@ inputs:
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
party_file: ./party.yml
`
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
t.Fatalf("write session.yml: %v", err)
@@ -67,7 +65,6 @@ inputs:
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
party_file: ./party.yml
`
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
t.Fatalf("write session.yml: %v", err)
@@ -96,7 +93,6 @@ inputs:
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
party_file: ./party.yml
`
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
t.Fatalf("write session.yml: %v", err)
@@ -146,7 +142,6 @@ inputs:
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
party_file: ./party.yml
`
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
t.Fatalf("write session.yml: %v", err)
@@ -171,7 +166,6 @@ inputs:
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
party_file: ./party.yml
`
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
t.Fatalf("write session.yml: %v", err)

View File

@@ -845,23 +845,11 @@ func validateResolvedPartyInputs(cfg *Config) error {
if cfg == nil || cfg.Session == nil {
return fmt.Errorf("session config is required")
}
if cfg.Party.Mode == PartyModeCanonical {
if cfg.Party.Canonical == nil {
return fmt.Errorf("canonical party data is required")
}
if cfg.Party.Source.Source != "campaign_config" {
return fmt.Errorf("canonical party must be sourced by campaign_config")
}
if strings.TrimSpace(cfg.StableInputs.PlayersFile.Source) != "derived_from_party" || strings.TrimSpace(cfg.StableInputs.PlayersFile.Path) != "" {
return fmt.Errorf("canonical party requires derived players input")
}
return nil
if cfg.Party.Canonical == nil {
return fmt.Errorf("canonical party data is required")
}
if strings.TrimSpace(cfg.Session.Inputs.PlayersFile) == "" || strings.TrimSpace(cfg.StableInputs.PlayersFile.Path) == "" {
return fmt.Errorf("players input is required with a legacy party")
}
if strings.TrimSpace(cfg.Session.Inputs.PartyFile) == "" || strings.TrimSpace(cfg.StableInputs.PartyFile.Path) == "" {
return fmt.Errorf("party input is required with a legacy party")
if cfg.Party.Source.Source != "campaign_config" {
return fmt.Errorf("canonical party must be sourced by campaign_config")
}
return nil
}

View File

@@ -1,57 +0,0 @@
package stage
import (
"fmt"
"path/filepath"
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
// materializeLegacyPartyInputs preserves the removable compatibility path for
// opaque party and explicit players files. Remove it when legacy roster inputs
// are no longer supported.
func materializeLegacyPartyInputs(
store artifacts.Store,
cfg *config.Config,
inputsDir string,
party, players artifactpolicy.PreparedInputSourceDescriptor,
) ([]manifest.InputRecord, error) {
playersInput := stableInputSource(cfg.StableInputs.PlayersFile, cfg.Session.Inputs.PlayersFile, cfg.SessionPath)
partyInput := stableInputSource(cfg.StableInputs.PartyFile, cfg.Session.Inputs.PartyFile, cfg.SessionPath)
playersSource, err := resolveConfigRelativePath(playersInput)
if err != nil {
return nil, fmt.Errorf("players path: %w", err)
}
partySource, err := resolveConfigRelativePath(partyInput)
if err != nil {
return nil, fmt.Errorf("party path: %w", err)
}
for _, required := range []struct {
path string
name string
}{
{path: playersSource, name: "players.yml"},
{path: partySource, name: "party.yml"},
} {
if err := requireFile(required.path, required.name); err != nil {
return nil, err
}
}
partyPath := filepath.Join(inputsDir, party.Filename)
partyChecksum, err := copyFileIfChanged(store, partySource, partyPath)
if err != nil {
return nil, fmt.Errorf("materialize party: %w", err)
}
playersPath := filepath.Join(inputsDir, players.Filename)
playersChecksum, err := copyFileIfChanged(store, playersSource, playersPath)
if err != nil {
return nil, fmt.Errorf("materialize players: %w", err)
}
return []manifest.InputRecord{
{Kind: party.ManifestKind, Path: partyPath, Checksum: partyChecksum, Source: partyInput.Source},
{Kind: players.ManifestKind, Path: playersPath, Checksum: playersChecksum, Source: playersInput.Source},
}, nil
}

View File

@@ -23,10 +23,7 @@ func materializePreparedPartyInputs(store artifacts.Store, cfg *config.Config, i
if !ok {
return nil, fmt.Errorf("prepared-input descriptor %q is unavailable", artifactpolicy.SourceInputParty)
}
if cfg.Party.Mode == config.PartyModeCanonical {
return materializeCanonicalPartyInputs(store, cfg.Party, inputsDir, party, players)
}
return materializeLegacyPartyInputs(store, cfg, inputsDir, party, players)
return materializeCanonicalPartyInputs(store, cfg.Party, inputsDir, party, players)
}
func materializeCanonicalPartyInputs(

View File

@@ -132,43 +132,17 @@ func currentPreparePartySourceInputs(env *Env) ([]prepareExpectedInput, error) {
if !ok {
return nil, fmt.Errorf("players prepared-input descriptor is unavailable")
}
if env.Config.Party.Mode == config.PartyModeCanonical {
if env.Config.Party.Canonical == nil || len(env.Config.Party.Canonical.Raw) == 0 {
return nil, fmt.Errorf("canonical party data is unavailable")
}
playersBytes, err := env.Config.Party.Canonical.PlayersYAML()
if err != nil {
return nil, err
}
return []prepareExpectedInput{
{kind: party.ManifestKind, checksum: checksumPrepareBytes(env.Config.Party.Canonical.Raw)},
{kind: players.ManifestKind, checksum: checksumPrepareBytes(playersBytes)},
}, nil
if env.Config.Party.Canonical == nil || len(env.Config.Party.Canonical.Raw) == 0 {
return nil, fmt.Errorf("canonical party data is unavailable")
}
cfg := env.Config
inputs := []struct {
kind string
resolved config.ResolvedInputFile
fallback string
}{
{kind: party.ManifestKind, resolved: cfg.StableInputs.PartyFile, fallback: cfg.Session.Inputs.PartyFile},
{kind: players.ManifestKind, resolved: cfg.StableInputs.PlayersFile, fallback: cfg.Session.Inputs.PlayersFile},
playersBytes, err := env.Config.Party.Canonical.PlayersYAML()
if err != nil {
return nil, err
}
expected := make([]prepareExpectedInput, 0, len(inputs))
for _, input := range inputs {
resolved := stableInputSource(input.resolved, input.fallback, cfg.SessionPath)
path, err := resolveConfigRelativePath(resolved)
if err != nil {
return nil, err
}
checksum, err := env.ArtifactStore.Checksum(path)
if err != nil {
return nil, err
}
expected = append(expected, prepareExpectedInput{kind: input.kind, checksum: checksum})
}
return expected, nil
return []prepareExpectedInput{
{kind: party.ManifestKind, checksum: checksumPrepareBytes(env.Config.Party.Canonical.Raw)},
{kind: players.ManifestKind, checksum: checksumPrepareBytes(playersBytes)},
}, nil
}
func currentPrepareAudioSourceInputs(ctx context.Context, env *Env, m *manifest.Manifest) ([]prepareExpectedInput, error) {

View File

@@ -18,9 +18,13 @@ func TestPrepareResumeValidatesCurrentLocalSourcesAndPreparedCopies(t *testing.T
mutate func(*testing.T, *Env, string)
}{
{
name: "stable input bytes changed",
name: "canonical party bytes changed",
mutate: func(t *testing.T, env *Env, _ string) {
writeFile(t, filepath.Join(filepath.Dir(env.Config.CampaignPath), "party.yml"), "changed: true\n")
party, err := config.ParseParty([]byte("schema_version: narratio.party.v1\ncharacters:\n changed:\n player: {name: Eric}\n character:\n name: Changed\n classes: [{name: wizard}]\n"))
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
env.Config.Party.Canonical = party
},
},
{
@@ -58,7 +62,7 @@ func TestPrepareResumeAcceptsRelocatedEquivalentLocalSource(t *testing.T) {
env, m, _ := prepareResumeLocalFixture(t)
cfgDir := filepath.Dir(env.Config.CampaignPath)
relocated := filepath.Join(cfgDir, "relocated-party.yml")
writeFile(t, relocated, "[]\n")
writeFile(t, relocated, "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
env.Config.StableInputs.PartyFile.Path = "./relocated-party.yml"
assertPrepareResume(t, env, m, true)
}
@@ -135,7 +139,7 @@ func setCanonicalPrepareParty(t *testing.T, env *Env, raw []byte) {
t.Fatalf("ParseParty() error = %v", err)
}
env.Config.Party = config.ResolvedParty{
Mode: config.PartyModeCanonical, Source: config.PartySource{Source: "campaign_config"}, Canonical: document.Canonical,
Source: config.PartySource{Source: "campaign_config"}, Canonical: document,
}
}

View File

@@ -177,19 +177,14 @@ characters:
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
if err := os.Remove(filepath.Join(filepath.Dir(env.Config.SessionPath), "players.yml")); err != nil {
t.Fatalf("remove legacy players source: %v", err)
}
env.Config.Party = config.ResolvedParty{
Mode: config.PartyModeCanonical,
Canonical: document.Canonical,
Canonical: document,
Source: config.PartySource{
Path: filepath.Join(filepath.Dir(env.Config.CampaignPath), "party.yml"),
ConfigPath: env.Config.CampaignPath,
Source: "campaign_config",
},
}
env.Config.StableInputs.PlayersFile = config.ResolvedInputFile{Source: "derived_from_party"}
writeFile(t, filepath.Join(filepath.Dir(env.Config.SessionPath), "audio", "alice.flac"), "audio")
if _, err := (prepareStage{}).Run(context.Background(), env, m); err != nil {
@@ -201,7 +196,7 @@ characters:
if got, err := os.ReadFile(preparedParty); err != nil || !bytes.Equal(got, partyBytes) {
t.Fatalf("prepared party = %q, %v; want unchanged canonical bytes", got, err)
}
wantPlayers, err := document.Canonical.PlayersYAML()
wantPlayers, err := document.PlayersYAML()
if err != nil {
t.Fatalf("PlayersYAML() error = %v", err)
}
@@ -228,30 +223,6 @@ characters:
}
}
func TestPrepareStageLegacyPartyInputsRemainOpaqueCopies(t *testing.T) {
env, m := setupPrepareEnv(t)
root := filepath.Dir(env.Config.SessionPath)
playersBytes := []byte("legacy players\n")
partyBytes := []byte("legacy party\n")
writeFile(t, filepath.Join(root, "players.yml"), string(playersBytes))
writeFile(t, filepath.Join(root, "party.yml"), string(partyBytes))
writeFile(t, filepath.Join(root, "audio", "alice.flac"), "audio")
env.Config.Party = config.ResolvedParty{Mode: config.PartyModeLegacy}
if _, err := (prepareStage{}).Run(context.Background(), env, m); err != nil {
t.Fatalf("prepare.Run() error = %v", err)
}
paths := sessionPathsForEnv(env, m.SessionID)
if got, err := os.ReadFile(filepath.Join(paths.InputsDir, "party.yml")); err != nil || !bytes.Equal(got, partyBytes) {
t.Fatalf("prepared legacy party = %q, %v", got, err)
}
if got, err := os.ReadFile(filepath.Join(paths.InputsDir, "players.yml")); err != nil || !bytes.Equal(got, playersBytes) {
t.Fatalf("prepared legacy players = %q, %v", got, err)
}
if got := findManifestInput(t, m.Inputs, "players").Source; got != "campaign_config" {
t.Fatalf("legacy players source = %q, want campaign_config", got)
}
}
func TestPrepareStageMaterializesSpellCatalogWithProvenance(t *testing.T) {
tests := []struct {
name string
@@ -888,15 +859,18 @@ inputs:
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
players_file: ./players.yml
party_file: ./party.yml
`)
writeFile(t, sessionPath, "session_id: 2026-05-03\n")
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
writeFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
writeFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
partyBytes := []byte("schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
writeFile(t, filepath.Join(cfgDir, "party.yml"), string(partyBytes))
party, err := config.ParseParty(partyBytes)
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
cfg := &config.Config{
Pipeline: &config.PipelineConfig{
@@ -907,6 +881,9 @@ inputs:
SessionPath: sessionPath,
CampaignPath: campaignPath,
PipelinePath: pipelinePath,
Party: config.ResolvedParty{Canonical: party, Source: config.PartySource{
Path: filepath.Join(cfgDir, "party.yml"), ConfigPath: campaignPath, Source: "campaign_config",
}},
Session: &config.SessionConfig{
SessionID: "2026-05-03",
Campaign: "sample-campaign",
@@ -930,11 +907,6 @@ inputs:
ConfigPath: campaignPath,
Source: "campaign_config",
},
PlayersFile: config.ResolvedInputFile{
Path: "./players.yml",
ConfigPath: campaignPath,
Source: "campaign_config",
},
PartyFile: config.ResolvedInputFile{
Path: "./party.yml",
ConfigPath: campaignPath,

View File

@@ -20,7 +20,6 @@ type prepareSemanticConfig struct {
CampaignID string `json:"campaign_id"`
SessionID string `json:"session_id"`
PreviousSession string `json:"previous_session_id"`
PartyMode string `json:"party_mode"`
PlayersSchema string `json:"players_schema_version,omitempty"`
Audio prepareAudioSelection `json:"audio"`
StableInputs []prepareStableInputSelection `json:"stable_inputs"`
@@ -106,21 +105,17 @@ func buildPrepareSemanticConfig(env *Env) (prepareSemanticConfig, error) {
if err != nil {
return prepareSemanticConfig{}, fmt.Errorf("resolve previous artifact requirements: %w", err)
}
partyMode := cfg.Party.Mode
if partyMode == "" {
if cfg.Party.Canonical == nil {
return prepareSemanticConfig{}, fmt.Errorf("prepare semantic configuration requires a resolved canonical party")
}
payload := prepareSemanticConfig{
CampaignID: strings.TrimSpace(cfg.Session.Campaign),
SessionID: strings.TrimSpace(cfg.Session.SessionID),
PreviousSession: strings.TrimSpace(cfg.Session.PreviousSessionID),
PartyMode: string(partyMode),
PlayersSchema: config.PlayersSchemaVersion,
Audio: prepareAudioSemantics(cfg),
StableInputs: prepareStableInputSemantics(cfg),
}
if partyMode == config.PartyModeCanonical {
payload.PlayersSchema = config.PlayersSchemaVersion
}
for _, requirement := range requirements {
entry := preparePreviousRequirement{Name: strings.TrimSpace(requirement.Name), Required: requirement.Required}
if cfg.Pipeline.Scriptorium != nil {
@@ -159,8 +154,7 @@ func prepareStableInputSemantics(cfg *config.Config) []prepareStableInputSelecti
prepareStableInput("speakers", stable.SpeakersFile, cfg.Session.Inputs.SpeakersFile),
prepareStableInput("autocorrect", stable.AutocorrectFile, cfg.Session.Inputs.AutocorrectFile),
prepareStableInput("glossary", stable.GlossaryFile, cfg.Session.Inputs.GlossaryFile),
prepareStableInput("players", stable.PlayersFile, cfg.Session.Inputs.PlayersFile),
prepareStableInput("party", stable.PartyFile, cfg.Session.Inputs.PartyFile),
prepareStableInput("party", stable.PartyFile, ""),
prepareStableInput("spell_catalog", stable.SpellCatalogFile, cfg.Session.Inputs.SpellCatalogFile),
}
}

View File

@@ -49,12 +49,11 @@ func TestPrepareSemanticConfigSensitivity(t *testing.T) {
func TestPrepareSemanticConfigRecordsCanonicalProjectionVersion(t *testing.T) {
env := initialSemanticEnv()
env.Config.Party.Mode = config.PartyModeCanonical
payload, err := buildPrepareSemanticConfig(env)
if err != nil {
t.Fatalf("buildPrepareSemanticConfig() error = %v", err)
}
if payload.PartyMode != string(config.PartyModeCanonical) || payload.PlayersSchema != config.PlayersSchemaVersion {
if payload.PlayersSchema != config.PlayersSchemaVersion {
t.Fatalf("prepare semantic payload = %#v", payload)
}
}
@@ -120,13 +119,13 @@ func initialSemanticEnv() *Env {
Session: &config.SessionConfig{
SessionID: "2026-05-03", Campaign: "campaign",
Inputs: config.SessionInputsConfig{
AudioFiles: []string{"/audio/speaker.flac"}, PartyFile: "party.yml",
AudioFiles: []string{"/audio/speaker.flac"},
},
},
StableInputs: config.ResolvedStableInputs{
PartyFile: config.ResolvedInputFile{Path: "/campaign/party.yml", ConfigPath: "/campaign/campaign.yml", Source: "session_config"},
},
Party: config.ResolvedParty{Mode: config.PartyModeCanonical},
Party: config.ResolvedParty{Canonical: &config.CanonicalParty{}},
}}
}

View File

@@ -401,12 +401,16 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
campaignPath := filepath.Join(cfgDir, "campaign.yml")
writeFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\n")
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
writeFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n")
writeFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n")
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
writeFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
writeFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
partyBytes := []byte("schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
writeFile(t, filepath.Join(cfgDir, "party.yml"), string(partyBytes))
party, err := config.ParseParty(partyBytes)
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
retries := 3
concurrency := 2
@@ -415,6 +419,9 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
CampaignPath: campaignPath,
SessionPath: sessionPath,
Campaign: &config.CampaignConfig{CampaignID: "sample-campaign"},
Party: config.ResolvedParty{Canonical: party, Source: config.PartySource{
Path: filepath.Join(cfgDir, "party.yml"), ConfigPath: campaignPath, Source: "campaign_config",
}},
Pipeline: &config.PipelineConfig{
Workspace: config.WorkspaceConfig{Root: workspace},
WhisperX: config.WhisperXConfig{
@@ -434,15 +441,12 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
SpeakersFile: "./speakers.yml",
AutocorrectFile: "./autocorrect.yml",
GlossaryFile: "./glossary.yml",
PlayersFile: "./players.yml",
PartyFile: "./party.yml",
},
},
StableInputs: config.ResolvedStableInputs{
SpeakersFile: config.ResolvedInputFile{Path: "./speakers.yml", ConfigPath: campaignPath, Source: "campaign_config"},
AutocorrectFile: config.ResolvedInputFile{Path: "./autocorrect.yml", ConfigPath: campaignPath, Source: "campaign_config"},
GlossaryFile: config.ResolvedInputFile{Path: "./glossary.yml", ConfigPath: campaignPath, Source: "campaign_config"},
PlayersFile: config.ResolvedInputFile{Path: "./players.yml", ConfigPath: campaignPath, Source: "campaign_config"},
PartyFile: config.ResolvedInputFile{Path: "./party.yml", ConfigPath: campaignPath, Source: "campaign_config"},
},
}