Remove legacy party configuration
This commit is contained in:
@@ -106,10 +106,10 @@ family declarations, and runtime provenance.
|
|||||||
|
|
||||||
`config sources` reports the same fully validated resolution without printing
|
`config sources` reports the same fully validated resolution without printing
|
||||||
effective values. Its header identifies the root, ordered imports, selected
|
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`.
|
remaining tab-separated records are sorted as `path`, `role`, and `source`.
|
||||||
Roles distinguish root, import, profile, centralized default, campaign, party,
|
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
|
one family record and one party record at the same logical path. The output
|
||||||
never reads or prints secret values.
|
never reads or prints secret values.
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ Use the [production/testing bundle](../examples/production-testing/pipeline.yml)
|
|||||||
as the complete copyable migration reference. Split stable pipeline settings
|
as the complete copyable migration reference. Split stable pipeline settings
|
||||||
into explicit additive imports, place production/testing differences in one
|
into explicit additive imports, place production/testing differences in one
|
||||||
selected overlay, and retain a production `default_profile`. Convert campaign
|
selected overlay, and retain a production `default_profile`. Convert campaign
|
||||||
rosters to [canonical party input](integrations/party.md), remove a separate
|
rosters to [canonical party input](integrations/party.md), then express
|
||||||
`players_file`, then express character work as families. Inspect the result
|
character work as families. Inspect the result
|
||||||
with `config validate`, `config show`, and `config sources`; use `config diff`
|
with `config validate`, `config show`, and `config sources`; use `config diff`
|
||||||
to review profiles before running a session. Unversioned party documents are
|
to review profiles before running a session. Unversioned party documents are
|
||||||
rejected.
|
rejected.
|
||||||
@@ -112,13 +112,13 @@ remote state with an unsafe legacy identity must be migrated before use.
|
|||||||
- Pipeline defaults are applied before validation.
|
- Pipeline defaults are applied before validation.
|
||||||
- Campaign and session identities must agree.
|
- Campaign and session identities must agree.
|
||||||
- Required stable files (`speakers_file`, `autocorrect_file`, `glossary_file`,
|
- Required stable files (`speakers_file`, `autocorrect_file`, `glossary_file`,
|
||||||
`party_file`) and the optional `spell_catalog_file` resolve from session
|
`party_file`) are campaign-owned; the first three may be overridden by the
|
||||||
overrides when provided, otherwise from campaign defaults. An empty or
|
session. The optional `spell_catalog_file` also inherits a campaign value
|
||||||
omitted session spell-catalog value inherits the campaign value.
|
when omitted by the session.
|
||||||
- `party_file` is parsed when pipeline and campaign configuration are combined.
|
- `party_file` is parsed when pipeline and campaign configuration are combined.
|
||||||
It must be a versioned [canonical party](integrations/party.md), is
|
It must be a versioned [canonical party](integrations/party.md), is
|
||||||
campaign-owned, derives the players input internally, and forbids both a
|
campaign-owned and derives the players input internally. `players_file` and
|
||||||
separate `players_file` and a session `party_file` override.
|
session `party_file` are unsupported configuration keys.
|
||||||
- Exactly one audio mode must be configured in session input:
|
- Exactly one audio mode must be configured in session input:
|
||||||
- local (`audio_dir` or `audio_files`), or
|
- local (`audio_dir` or `audio_files`), or
|
||||||
- S3 (`audio_s3.prefix`).
|
- S3 (`audio_s3.prefix`).
|
||||||
@@ -521,7 +521,6 @@ integration.
|
|||||||
| `inputs.speakers_file` | string | Yes | stable input default |
|
| `inputs.speakers_file` | string | Yes | stable input default |
|
||||||
| `inputs.autocorrect_file` | string | Yes | stable input default |
|
| `inputs.autocorrect_file` | string | Yes | stable input default |
|
||||||
| `inputs.glossary_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.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` |
|
| `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.speakers_file` | string | No | overrides campaign stable input |
|
||||||
| `inputs.autocorrect_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.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.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_dir` | string | Conditional | local audio mode |
|
||||||
| `inputs.audio_files[]` | list[string] | Conditional | local audio mode |
|
| `inputs.audio_files[]` | list[string] | Conditional | local audio mode |
|
||||||
|
|||||||
@@ -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
|
Unversioned party inputs are rejected. Replace each legacy party document with
|
||||||
the versioned structure above, move each roster entry under `characters`, and
|
the versioned structure above, move each roster entry under `characters`, and
|
||||||
remove separate player and session-party overrides from the selected campaign
|
remove separate player and session-party settings: those keys are unsupported.
|
||||||
and session configuration. The configuration-key removal itself is staged
|
|
||||||
separately; current configurations must already omit those competing values
|
|
||||||
when using a canonical party.
|
|
||||||
|
|
||||||
The configuration rules and source-relative path behavior are defined in the
|
The configuration rules and source-relative path behavior are defined in the
|
||||||
[Configuration Reference](../config.md).
|
[Configuration Reference](../config.md).
|
||||||
|
|||||||
@@ -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
|
Campaign context construction reads the campaign-owned party source through
|
||||||
`ParseParty`. The required canonical party retains its raw bytes and normalized
|
`ParseParty`. The required canonical party retains its raw bytes and normalized
|
||||||
roster in runtime-only `ResolvedParty` provenance. Canonical resolution creates
|
roster in runtime-only `ResolvedParty` provenance. Canonical resolution creates
|
||||||
a virtual `derived_from_party` players input and rejects competing campaign or
|
a deterministic `derived_from_party` players projection. The party is the only
|
||||||
session players files and session party overrides.
|
configured roster authority; player and session-party fields are unsupported.
|
||||||
|
|
||||||
## Canonical Party Domain
|
## Canonical Party Domain
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ fields and mode shell that supported them.
|
|||||||
|
|
||||||
## Stage 3 — Remove Legacy Party And Players Configuration
|
## Stage 3 — Remove Legacy Party And Players Configuration
|
||||||
|
|
||||||
**Status: Pending**
|
**Status: Completed**
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ Safe fix:
|
|||||||
|
|
||||||
- replace the file with one exact, single-document
|
- replace the file with one exact, single-document
|
||||||
[`narratio.party.v1` party](./integrations/party.md); do not add a version
|
[`narratio.party.v1` party](./integrations/party.md); do not add a version
|
||||||
marker to an otherwise incompatible legacy structure. Remove competing
|
marker to an otherwise incompatible legacy structure. The versioned party is
|
||||||
player and session-party settings while migrating.
|
the only configured roster authority.
|
||||||
|
|
||||||
Relevant reference: [Canonical Party Input](./integrations/party.md).
|
Relevant reference: [Canonical Party Input](./integrations/party.md).
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
[party](campaigns/sample-campaign/party.yml) fixture, plus an optional
|
||||||
[spell-catalog overlay](campaigns/sample-campaign/spell_catalog.json) that
|
[spell-catalog overlay](campaigns/sample-campaign/spell_catalog.json) that
|
||||||
follows the Notarius v0.6 contract. Narratio derives the players projection
|
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
|
- [Sample speaker audio](audio/sample-speaker.flac) is a text placeholder that
|
||||||
reserves the expected filename and directory shape. Replace it with a real
|
reserves the expected filename and directory shape. Replace it with a real
|
||||||
FLAC file before running transcription.
|
FLAC file before running transcription.
|
||||||
|
|||||||
@@ -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 {
|
if _, err := fmt.Fprintf(out, "campaign: id=%s source=%s\n", config.CampaignID(resolved.Campaign), config.NormalizedConfigurationPath(resolved.CampaignPath)); err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ func TestConfigSourcesReportsStableOwnership(t *testing.T) {
|
|||||||
"imports: none",
|
"imports: none",
|
||||||
"profile: name=testing selection=cli overlay=",
|
"profile: name=testing selection=cli overlay=",
|
||||||
"campaign: id=sample-campaign source=",
|
"campaign: id=sample-campaign source=",
|
||||||
"party: mode=canonical source=",
|
"party: source=",
|
||||||
"digest: ",
|
"digest: ",
|
||||||
"path\trole\tsource\n",
|
"path\trole\tsource\n",
|
||||||
"workspace.root\troot\t",
|
"workspace.root\troot\t",
|
||||||
@@ -423,7 +423,7 @@ func TestConfigSourcesReportsCanonicalFamilyAndPublishOrigins(t *testing.T) {
|
|||||||
output := out.String()
|
output := out.String()
|
||||||
partyPath := filepath.Join(filepath.Dir(campaignPath), "party.yml")
|
partyPath := filepath.Join(filepath.Dir(campaignPath), "party.yml")
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"party: mode=canonical source=" + partyPath,
|
"party: source=" + partyPath,
|
||||||
"derived.players\tparty\t" + partyPath,
|
"derived.players\tparty\t" + partyPath,
|
||||||
"scriptorium.artifacts.character_note_arannis.enabled\tfamily\t" + pipelinePath,
|
"scriptorium.artifacts.character_note_arannis.enabled\tfamily\t" + pipelinePath,
|
||||||
"scriptorium.artifacts.character_note_arannis.enabled\tparty\t" + partyPath,
|
"scriptorium.artifacts.character_note_arannis.enabled\tparty\t" + partyPath,
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ characters:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseParty(changed party) error = %v", err)
|
t.Fatalf("ParseParty(changed party) error = %v", err)
|
||||||
}
|
}
|
||||||
cfg.Party.Canonical = partyDocument.Canonical
|
cfg.Party.Canonical = partyDocument
|
||||||
|
|
||||||
prepared := loadLifecycleManifest(t, cfg)
|
prepared := loadLifecycleManifest(t, cfg)
|
||||||
prepare, err := stage.Select("prepare")
|
prepare, err := stage.Select("prepare")
|
||||||
@@ -290,7 +290,7 @@ characters:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseParty(canonical party) error = %v", err)
|
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 {
|
if _, err := executeStages(context.Background(), cfg, prepareExtractLifecyclePlan(t), RunOptions{Env: env}); err != nil {
|
||||||
t.Fatalf("executeStages() error = %v", err)
|
t.Fatalf("executeStages() error = %v", err)
|
||||||
|
|||||||
@@ -56,17 +56,18 @@ func inspectStableInputs(cfg *config.Config) []stableInputCheck {
|
|||||||
name string
|
name string
|
||||||
in config.ResolvedInputFile
|
in config.ResolvedInputFile
|
||||||
optional bool
|
optional bool
|
||||||
|
derived bool
|
||||||
}{
|
}{
|
||||||
{name: "speakers", in: cfg.StableInputs.SpeakersFile},
|
{name: "speakers", in: cfg.StableInputs.SpeakersFile},
|
||||||
{name: "autocorrect", in: cfg.StableInputs.AutocorrectFile},
|
{name: "autocorrect", in: cfg.StableInputs.AutocorrectFile},
|
||||||
{name: "glossary", in: cfg.StableInputs.GlossaryFile},
|
{name: "glossary", in: cfg.StableInputs.GlossaryFile},
|
||||||
{name: "players", in: cfg.StableInputs.PlayersFile},
|
{name: "players", derived: true},
|
||||||
{name: "party", in: cfg.StableInputs.PartyFile},
|
{name: "party", in: cfg.StableInputs.PartyFile},
|
||||||
{name: "spell_catalog", in: cfg.StableInputs.SpellCatalogFile, optional: true},
|
{name: "spell_catalog", in: cfg.StableInputs.SpellCatalogFile, optional: true},
|
||||||
}
|
}
|
||||||
out := make([]stableInputCheck, 0, len(items))
|
out := make([]stableInputCheck, 0, len(items))
|
||||||
for _, item := range 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"})
|
out = append(out, stableInputCheck{Name: item.name, Path: "derived from canonical party"})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ inputs:
|
|||||||
t.Fatalf("loadCommandConfig() error = %v", err)
|
t.Fatalf("loadCommandConfig() error = %v", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = loaded.Close() }()
|
defer func() { _ = loaded.Close() }()
|
||||||
if loaded.Config.Party.Mode != config.PartyModeCanonical || loaded.Config.StableInputs.PlayersFile.Source != "derived_from_party" {
|
if loaded.Config.Party.Canonical == nil {
|
||||||
t.Fatalf("remote config party = %#v, players = %#v", loaded.Config.Party, loaded.Config.StableInputs.PlayersFile)
|
t.Fatalf("remote config party = %#v", loaded.Config.Party)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1335,8 +1335,7 @@ func testConfig(t *testing.T) *config.Config {
|
|||||||
CampaignPath: campaignPath,
|
CampaignPath: campaignPath,
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
Party: config.ResolvedParty{
|
Party: config.ResolvedParty{
|
||||||
Mode: config.PartyModeCanonical,
|
Canonical: partyDocument,
|
||||||
Canonical: partyDocument.Canonical,
|
|
||||||
Source: config.PartySource{
|
Source: config.PartySource{
|
||||||
Path: filepath.Join(cfgDir, "party.yml"),
|
Path: filepath.Join(cfgDir, "party.yml"),
|
||||||
ConfigPath: campaignPath,
|
ConfigPath: campaignPath,
|
||||||
@@ -1359,7 +1358,6 @@ func testConfig(t *testing.T) *config.Config {
|
|||||||
ConfigPath: campaignPath,
|
ConfigPath: campaignPath,
|
||||||
Source: "campaign_config",
|
Source: "campaign_config",
|
||||||
},
|
},
|
||||||
PlayersFile: config.ResolvedInputFile{Source: "derived_from_party"},
|
|
||||||
PartyFile: config.ResolvedInputFile{
|
PartyFile: config.ResolvedInputFile{
|
||||||
Path: "./party.yml",
|
Path: "./party.yml",
|
||||||
ConfigPath: campaignPath,
|
ConfigPath: campaignPath,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func expandPipelineArtifactFamilies(cfg *PipelineConfig, party ResolvedParty) er
|
|||||||
if len(families) == 0 {
|
if len(families) == 0 {
|
||||||
return nil
|
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")
|
return fmt.Errorf("pipeline.scriptorium.artifact_families requires a canonical campaign party")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func TestArtifactFamiliesRejectGeneratedKeyAndOutputCollisions(t *testing.T) {
|
|||||||
Scriptorium: &ScriptoriumConfig{Artifacts: test.artifacts},
|
Scriptorium: &ScriptoriumConfig{Artifacts: test.artifacts},
|
||||||
resolution: &pipelineResolutionMetadata{artifactFamilies: map[string]ScriptoriumArtifactFamilyConfig{"character_meta": base}},
|
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) {
|
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
||||||
t.Fatalf("expandPipelineArtifactFamilies() error = %v, want %q", err, test.wantError)
|
t.Fatalf("expandPipelineArtifactFamilies() error = %v, want %q", err, test.wantError)
|
||||||
}
|
}
|
||||||
@@ -290,12 +290,12 @@ func TestArtifactFamiliesRejectInvalidMemberDependenciesAndSources(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
t.Fatalf("concrete member source error = %v", err)
|
||||||
}
|
}
|
||||||
pipeline.Scriptorium.Artifacts["explicit"] = ScriptoriumArtifactConfig{Inputs: map[string]ScriptoriumInputConfig{"bad": {Source: "narratio.member_artifact."}}}
|
pipeline.Scriptorium.Artifacts["explicit"] = ScriptoriumArtifactConfig{Inputs: map[string]ScriptoriumInputConfig{"bad": {Source: "narratio.member_artifact."}}}
|
||||||
pipeline.resolution.artifactFamiliesExpanded = false
|
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)
|
t.Fatalf("malformed concrete member source error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
|||||||
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./campaign-speakers.yml", campaignPath, "campaign_config")
|
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.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
||||||
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.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")
|
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.SpeakersFile, "./session-speakers.yml", sessionPath, "session_config")
|
||||||
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.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.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")
|
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./campaign-party.yml", campaignPath, "campaign_config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ type CampaignInputsConfig struct {
|
|||||||
SpeakersFile string `yaml:"speakers_file"`
|
SpeakersFile string `yaml:"speakers_file"`
|
||||||
AutocorrectFile string `yaml:"autocorrect_file"`
|
AutocorrectFile string `yaml:"autocorrect_file"`
|
||||||
GlossaryFile string `yaml:"glossary_file"`
|
GlossaryFile string `yaml:"glossary_file"`
|
||||||
PlayersFile string `yaml:"players_file"`
|
|
||||||
PartyFile string `yaml:"party_file"`
|
PartyFile string `yaml:"party_file"`
|
||||||
SpellCatalogFile string `yaml:"spell_catalog_file"`
|
SpellCatalogFile string `yaml:"spell_catalog_file"`
|
||||||
}
|
}
|
||||||
@@ -357,8 +356,6 @@ type SessionInputsConfig struct {
|
|||||||
SpeakersFile string `yaml:"speakers_file"`
|
SpeakersFile string `yaml:"speakers_file"`
|
||||||
AutocorrectFile string `yaml:"autocorrect_file"`
|
AutocorrectFile string `yaml:"autocorrect_file"`
|
||||||
GlossaryFile string `yaml:"glossary_file"`
|
GlossaryFile string `yaml:"glossary_file"`
|
||||||
PlayersFile string `yaml:"players_file"`
|
|
||||||
PartyFile string `yaml:"party_file"`
|
|
||||||
SpellCatalogFile string `yaml:"spell_catalog_file"`
|
SpellCatalogFile string `yaml:"spell_catalog_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +370,6 @@ type ResolvedStableInputs struct {
|
|||||||
SpeakersFile ResolvedInputFile
|
SpeakersFile ResolvedInputFile
|
||||||
AutocorrectFile ResolvedInputFile
|
AutocorrectFile ResolvedInputFile
|
||||||
GlossaryFile ResolvedInputFile
|
GlossaryFile ResolvedInputFile
|
||||||
PlayersFile ResolvedInputFile
|
|
||||||
PartyFile ResolvedInputFile
|
PartyFile ResolvedInputFile
|
||||||
SpellCatalogFile ResolvedInputFile
|
SpellCatalogFile ResolvedInputFile
|
||||||
}
|
}
|
||||||
@@ -385,11 +381,10 @@ type ResolvedInputFile struct {
|
|||||||
Source string
|
Source string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedParty records the selected party mode, its non-secret source
|
// ResolvedParty records the campaign-owned canonical party and its non-secret
|
||||||
// provenance, and canonical domain data when available. It is runtime-only and
|
// source provenance. It is runtime-only and must never be copied into manifests
|
||||||
// must never be copied into manifests as raw party content.
|
// as raw party content.
|
||||||
type ResolvedParty struct {
|
type ResolvedParty struct {
|
||||||
Mode PartyMode
|
|
||||||
Source PartySource
|
Source PartySource
|
||||||
Canonical *CanonicalParty
|
Canonical *CanonicalParty
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,8 @@ func EffectivePipelineSources(cfg *PipelineConfig, party ResolvedParty) ([]Effec
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EffectiveCampaignSources projects selected campaign and party ownership
|
// EffectiveCampaignSources projects selected campaign and party ownership
|
||||||
// without retaining or rendering campaign values. Canonical players are a
|
// without retaining or rendering campaign values. Players are derived from the
|
||||||
// derived party value, while a legacy players file remains explicitly marked
|
// canonical party source.
|
||||||
// as legacy input provenance.
|
|
||||||
func EffectiveCampaignSources(campaignPath string, campaign *CampaignConfig, party ResolvedParty) []EffectivePipelineSourceRecord {
|
func EffectiveCampaignSources(campaignPath string, campaign *CampaignConfig, party ResolvedParty) []EffectivePipelineSourceRecord {
|
||||||
if campaign == nil {
|
if campaign == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -143,21 +142,11 @@ func EffectiveCampaignSources(campaignPath string, campaign *CampaignConfig, par
|
|||||||
if strings.TrimSpace(campaign.Inputs.SpellCatalogFile) != "" {
|
if strings.TrimSpace(campaign.Inputs.SpellCatalogFile) != "" {
|
||||||
records = append(records, EffectivePipelineSourceRecord{Path: "campaign.inputs.spell_catalog_file", Role: "campaign", Source: campaignSource})
|
records = append(records, EffectivePipelineSourceRecord{Path: "campaign.inputs.spell_catalog_file", Role: "campaign", Source: campaignSource})
|
||||||
}
|
}
|
||||||
if party.Mode == PartyModeCanonical {
|
partySource := normalizedProvenancePath(party.Source.Path)
|
||||||
partySource := normalizedProvenancePath(party.Source.Path)
|
records = append(records,
|
||||||
records = append(records,
|
EffectivePipelineSourceRecord{Path: "campaign.inputs.party_file", Role: "party", Source: partySource},
|
||||||
EffectivePipelineSourceRecord{Path: "campaign.inputs.party_file", Role: "party", Source: partySource},
|
EffectivePipelineSourceRecord{Path: "derived.players", 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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Slice(records, func(left, right int) bool {
|
sort.Slice(records, func(left, right int) bool {
|
||||||
if records[left].Path != records[right].Path {
|
if records[left].Path != records[right].Path {
|
||||||
return records[left].Path < records[right].Path
|
return records[left].Path < records[right].Path
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ func TestIdentityValidationRejectsUnsafeSegments(t *testing.T) {
|
|||||||
for _, value := range unsafe {
|
for _, value := range unsafe {
|
||||||
t.Run("campaign/"+strings.ReplaceAll(value, "/", "_"), func(t *testing.T) {
|
t.Run("campaign/"+strings.ReplaceAll(value, "/", "_"), func(t *testing.T) {
|
||||||
cfg := &CampaignConfig{CampaignID: value, Inputs: CampaignInputsConfig{
|
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 {
|
if err := validateCampaign(cfg); err == nil {
|
||||||
t.Fatalf("validateCampaign(%q) error = nil, want rejection", value)
|
t.Fatalf("validateCampaign(%q) error = nil, want rejection", value)
|
||||||
|
|||||||
@@ -278,14 +278,9 @@ func LoadPipelineCampaignWithParty(pipelinePath string, pipeline *PipelineConfig
|
|||||||
if campaign == nil {
|
if campaign == nil {
|
||||||
return LoadedPipelineCampaign{}, fmt.Errorf("campaign config is required")
|
return LoadedPipelineCampaign{}, fmt.Errorf("campaign config is required")
|
||||||
}
|
}
|
||||||
if party.Mode == "" {
|
if party.Canonical == nil {
|
||||||
return LoadedPipelineCampaign{}, fmt.Errorf("resolved campaign party is required")
|
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 {
|
if err := expandPipelineArtifactFamilies(pipeline, party); err != nil {
|
||||||
return LoadedPipelineCampaign{}, err
|
return LoadedPipelineCampaign{}, err
|
||||||
}
|
}
|
||||||
@@ -322,18 +317,13 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
|
|||||||
if loaded.Campaign == nil {
|
if loaded.Campaign == nil {
|
||||||
return nil, fmt.Errorf("campaign config is required")
|
return nil, fmt.Errorf("campaign config is required")
|
||||||
}
|
}
|
||||||
if loaded.Party.Mode == "" {
|
if loaded.Party.Canonical == nil {
|
||||||
party, err := resolveCampaignParty(loaded.CampaignPath, loaded.Campaign)
|
party, err := resolveCampaignParty(loaded.CampaignPath, loaded.Campaign)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
loaded.Party = party
|
loaded.Party = party
|
||||||
}
|
}
|
||||||
if loaded.Party.Mode == PartyModeCanonical {
|
|
||||||
if err := validateCanonicalPartySelection(loaded.Campaign, nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Pipeline: loaded.Pipeline,
|
Pipeline: loaded.Pipeline,
|
||||||
Campaign: loaded.Campaign,
|
Campaign: loaded.Campaign,
|
||||||
@@ -344,11 +334,6 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
|
|||||||
SessionSource: sessionSource,
|
SessionSource: sessionSource,
|
||||||
Party: loaded.Party,
|
Party: loaded.Party,
|
||||||
}
|
}
|
||||||
if cfg.Party.Mode == PartyModeCanonical && sessionCfg != nil {
|
|
||||||
if err := validateCanonicalPartySelection(loaded.Campaign, sessionCfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sessionCfg == nil {
|
if sessionCfg == nil {
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
@@ -363,22 +348,6 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
|
|||||||
if strings.TrimSpace(cfg.SessionSource.LocalPath) == "" {
|
if strings.TrimSpace(cfg.SessionSource.LocalPath) == "" {
|
||||||
cfg.SessionSource.LocalPath = sessionPath
|
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
|
cfg.StableInputs = stableInputs
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
@@ -456,15 +425,9 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
|
|||||||
campaignPath,
|
campaignPath,
|
||||||
sessionPath,
|
sessionPath,
|
||||||
),
|
),
|
||||||
PlayersFile: selectStableInput(
|
|
||||||
campaignCfg.Inputs.PlayersFile,
|
|
||||||
sessionCfg.Inputs.PlayersFile,
|
|
||||||
campaignPath,
|
|
||||||
sessionPath,
|
|
||||||
),
|
|
||||||
PartyFile: selectStableInput(
|
PartyFile: selectStableInput(
|
||||||
campaignCfg.Inputs.PartyFile,
|
campaignCfg.Inputs.PartyFile,
|
||||||
sessionCfg.Inputs.PartyFile,
|
"",
|
||||||
campaignPath,
|
campaignPath,
|
||||||
sessionPath,
|
sessionPath,
|
||||||
),
|
),
|
||||||
@@ -479,8 +442,6 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
|
|||||||
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
||||||
sessionCfg.Inputs.AutocorrectFile = stable.AutocorrectFile.Path
|
sessionCfg.Inputs.AutocorrectFile = stable.AutocorrectFile.Path
|
||||||
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.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
|
sessionCfg.Inputs.SpellCatalogFile = stable.SpellCatalogFile.Path
|
||||||
return stable, nil
|
return stable, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" {
|
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)
|
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)
|
t.Fatalf("canonical party = %#v", cfg.Party)
|
||||||
}
|
}
|
||||||
characters := cfg.Party.Canonical.Characters
|
characters := cfg.Party.Canonical.Characters
|
||||||
|
|||||||
@@ -20,35 +20,6 @@ const (
|
|||||||
PlayersSchemaVersion = "narratio.players.v1"
|
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
|
// CanonicalParty is the normalized domain value for narratio.party.v1. Raw is
|
||||||
// retained independently so later preparation can copy the authored party file
|
// retained independently so later preparation can copy the authored party file
|
||||||
// byte-for-byte rather than serializing normalized values.
|
// byte-for-byte rather than serializing normalized values.
|
||||||
@@ -84,7 +55,7 @@ type PartyClass struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseParty strictly parses one supported party document.
|
// ParseParty strictly parses one supported party document.
|
||||||
func ParseParty(data []byte) (*PartyDocument, error) {
|
func ParseParty(data []byte) (*CanonicalParty, error) {
|
||||||
root, decoder, err := parsePartyFirstDocument(data)
|
root, decoder, err := parsePartyFirstDocument(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -96,7 +67,7 @@ func ParseParty(data []byte) (*PartyDocument, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &PartyDocument{Mode: PartyModeCanonical, Canonical: party}, nil
|
return party, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePartyFirstDocument(data []byte) (*yaml.Node, *yaml.Decoder, error) {
|
func parsePartyFirstDocument(data []byte) (*yaml.Node, *yaml.Decoder, error) {
|
||||||
|
|||||||
@@ -28,18 +28,17 @@ func resolvePartyInput(input ResolvedInputFile) (ResolvedParty, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ResolvedParty{}, fmt.Errorf("read party input %q: %w", path, err)
|
return ResolvedParty{}, fmt.Errorf("read party input %q: %w", path, err)
|
||||||
}
|
}
|
||||||
document, err := ParseParty(raw)
|
party, err := ParseParty(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ResolvedParty{}, fmt.Errorf("parse party input %q: %w", path, err)
|
return ResolvedParty{}, fmt.Errorf("parse party input %q: %w", path, err)
|
||||||
}
|
}
|
||||||
return ResolvedParty{
|
return ResolvedParty{
|
||||||
Mode: PartyModeCanonical,
|
|
||||||
Source: PartySource{
|
Source: PartySource{
|
||||||
Path: path,
|
Path: path,
|
||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
Source: input.Source,
|
Source: input.Source,
|
||||||
},
|
},
|
||||||
Canonical: document.Canonical,
|
Canonical: party,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,26 +63,3 @@ func resolveCampaignParty(campaignPath string, campaign *CampaignConfig) (Resolv
|
|||||||
}
|
}
|
||||||
return resolvePartyInput(input)
|
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"}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ inputs:
|
|||||||
if err := Validate(cfg); err != nil {
|
if err := Validate(cfg); err != nil {
|
||||||
t.Fatalf("Validate() error = %v", err)
|
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)
|
t.Fatalf("party = %#v, want canonical party", cfg.Party)
|
||||||
}
|
}
|
||||||
if got, want := cfg.Party.Source.Path, partyPath; got != want {
|
if got, want := cfg.Party.Source.Path, partyPath; got != want {
|
||||||
@@ -56,9 +56,6 @@ inputs:
|
|||||||
if !bytes.Equal(cfg.Party.Canonical.Raw, partyYAML) {
|
if !bytes.Equal(cfg.Party.Canonical.Raw, partyYAML) {
|
||||||
t.Fatal("canonical party bytes were not retained")
|
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) {
|
func TestCanonicalPartyRejectsSeparatePlayersAndSessionPartyOverrides(t *testing.T) {
|
||||||
@@ -68,9 +65,9 @@ func TestCanonicalPartyRejectsSeparatePlayersAndSessionPartyOverrides(t *testing
|
|||||||
sessionExtra string
|
sessionExtra string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{name: "campaign players", campaignExtra: " players_file: players.yml\n", want: "campaign.inputs.players_file is not allowed"},
|
{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: "session.inputs.players_file is not allowed"},
|
{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: "session.inputs.party_file cannot override"},
|
{name: "session party", sessionExtra: " party_file: party-override.yml\n", want: "field party_file not found"},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
@@ -126,15 +123,15 @@ func TestResolveAndLoadedCampaignShareCanonicalPartyResolution(t *testing.T) {
|
|||||||
t.Fatalf("LoadPipelineCampaign() error = %v", err)
|
t.Fatalf("LoadPipelineCampaign() error = %v", err)
|
||||||
}
|
}
|
||||||
partial, err := ResolveLoadedPipelineCampaign(loaded, "", nil, SessionSource{})
|
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)
|
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})
|
direct, err := Resolve(pipelinePath, pipeline, campaignPath, campaign, sessionPath, session, SessionSource{Source: "session_config", LocalPath: sessionPath})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resolve() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
if direct.Party.Mode != PartyModeCanonical || direct.StableInputs.PlayersFile.Source != "derived_from_party" {
|
if direct.Party.Canonical == nil {
|
||||||
t.Fatalf("Resolve() party = %#v, players = %#v", direct.Party, direct.StableInputs.PlayersFile)
|
t.Fatalf("Resolve() party = %#v", direct.Party)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,10 @@ characters:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseParty() error = %v", err)
|
t.Fatalf("ParseParty() error = %v", err)
|
||||||
}
|
}
|
||||||
if !document.IsCanonical() || document.Mode != PartyModeCanonical {
|
if !bytes.Equal(document.Raw, data) {
|
||||||
t.Fatalf("document mode = %#v, want canonical", document)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(document.Canonical.Raw, data) {
|
|
||||||
t.Fatal("canonical raw bytes were not preserved")
|
t.Fatal("canonical raw bytes were not preserved")
|
||||||
}
|
}
|
||||||
characters := document.Canonical.Characters
|
characters := document.Characters
|
||||||
if len(characters) != 1 || characters[0].ID != "arannis" {
|
if len(characters) != 1 || characters[0].ID != "arannis" {
|
||||||
t.Fatalf("characters = %#v, want ordered arannis character", characters)
|
t.Fatalf("characters = %#v, want ordered arannis character", characters)
|
||||||
}
|
}
|
||||||
@@ -190,7 +187,7 @@ characters:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseParty() error = %v", err)
|
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)
|
t.Fatalf("character count = %d, want 2", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +246,7 @@ characters:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("PlayersYAML() error = %v", err)
|
t.Fatalf("PlayersYAML() error = %v", err)
|
||||||
}
|
}
|
||||||
again, err := document.Canonical.PlayersYAML()
|
again, err := document.PlayersYAML()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("PlayersYAML() repeat error = %v", err)
|
t.Fatalf("PlayersYAML() repeat error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ inputs:
|
|||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
party_file: ./party.yml
|
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session.yml: %v", err)
|
t.Fatalf("write session.yml: %v", err)
|
||||||
@@ -41,7 +40,6 @@ inputs:
|
|||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
party_file: ./party.yml
|
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session.yml: %v", err)
|
t.Fatalf("write session.yml: %v", err)
|
||||||
@@ -67,7 +65,6 @@ inputs:
|
|||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
party_file: ./party.yml
|
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session.yml: %v", err)
|
t.Fatalf("write session.yml: %v", err)
|
||||||
@@ -96,7 +93,6 @@ inputs:
|
|||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
party_file: ./party.yml
|
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session.yml: %v", err)
|
t.Fatalf("write session.yml: %v", err)
|
||||||
@@ -146,7 +142,6 @@ inputs:
|
|||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
party_file: ./party.yml
|
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session.yml: %v", err)
|
t.Fatalf("write session.yml: %v", err)
|
||||||
@@ -171,7 +166,6 @@ inputs:
|
|||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
party_file: ./party.yml
|
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session.yml: %v", err)
|
t.Fatalf("write session.yml: %v", err)
|
||||||
|
|||||||
@@ -845,23 +845,11 @@ func validateResolvedPartyInputs(cfg *Config) error {
|
|||||||
if cfg == nil || cfg.Session == nil {
|
if cfg == nil || cfg.Session == nil {
|
||||||
return fmt.Errorf("session config is required")
|
return fmt.Errorf("session config is required")
|
||||||
}
|
}
|
||||||
if cfg.Party.Mode == PartyModeCanonical {
|
if cfg.Party.Canonical == nil {
|
||||||
if cfg.Party.Canonical == nil {
|
return fmt.Errorf("canonical party data is required")
|
||||||
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 strings.TrimSpace(cfg.Session.Inputs.PlayersFile) == "" || strings.TrimSpace(cfg.StableInputs.PlayersFile.Path) == "" {
|
if cfg.Party.Source.Source != "campaign_config" {
|
||||||
return fmt.Errorf("players input is required with a legacy party")
|
return fmt.Errorf("canonical party must be sourced by campaign_config")
|
||||||
}
|
|
||||||
if strings.TrimSpace(cfg.Session.Inputs.PartyFile) == "" || strings.TrimSpace(cfg.StableInputs.PartyFile.Path) == "" {
|
|
||||||
return fmt.Errorf("party input is required with a legacy party")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -23,10 +23,7 @@ func materializePreparedPartyInputs(store artifacts.Store, cfg *config.Config, i
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("prepared-input descriptor %q is unavailable", artifactpolicy.SourceInputParty)
|
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 materializeCanonicalPartyInputs(store, cfg.Party, inputsDir, party, players)
|
|
||||||
}
|
|
||||||
return materializeLegacyPartyInputs(store, cfg, inputsDir, party, players)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func materializeCanonicalPartyInputs(
|
func materializeCanonicalPartyInputs(
|
||||||
|
|||||||
@@ -132,43 +132,17 @@ func currentPreparePartySourceInputs(env *Env) ([]prepareExpectedInput, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("players prepared-input descriptor is unavailable")
|
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 {
|
||||||
if env.Config.Party.Canonical == nil || len(env.Config.Party.Canonical.Raw) == 0 {
|
return nil, fmt.Errorf("canonical party data is unavailable")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
playersBytes, err := env.Config.Party.Canonical.PlayersYAML()
|
||||||
cfg := env.Config
|
if err != nil {
|
||||||
inputs := []struct {
|
return nil, err
|
||||||
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},
|
|
||||||
}
|
}
|
||||||
expected := make([]prepareExpectedInput, 0, len(inputs))
|
return []prepareExpectedInput{
|
||||||
for _, input := range inputs {
|
{kind: party.ManifestKind, checksum: checksumPrepareBytes(env.Config.Party.Canonical.Raw)},
|
||||||
resolved := stableInputSource(input.resolved, input.fallback, cfg.SessionPath)
|
{kind: players.ManifestKind, checksum: checksumPrepareBytes(playersBytes)},
|
||||||
path, err := resolveConfigRelativePath(resolved)
|
}, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func currentPrepareAudioSourceInputs(ctx context.Context, env *Env, m *manifest.Manifest) ([]prepareExpectedInput, error) {
|
func currentPrepareAudioSourceInputs(ctx context.Context, env *Env, m *manifest.Manifest) ([]prepareExpectedInput, error) {
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ func TestPrepareResumeValidatesCurrentLocalSourcesAndPreparedCopies(t *testing.T
|
|||||||
mutate func(*testing.T, *Env, string)
|
mutate func(*testing.T, *Env, string)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "stable input bytes changed",
|
name: "canonical party bytes changed",
|
||||||
mutate: func(t *testing.T, env *Env, _ string) {
|
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)
|
env, m, _ := prepareResumeLocalFixture(t)
|
||||||
cfgDir := filepath.Dir(env.Config.CampaignPath)
|
cfgDir := filepath.Dir(env.Config.CampaignPath)
|
||||||
relocated := filepath.Join(cfgDir, "relocated-party.yml")
|
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"
|
env.Config.StableInputs.PartyFile.Path = "./relocated-party.yml"
|
||||||
assertPrepareResume(t, env, m, true)
|
assertPrepareResume(t, env, m, true)
|
||||||
}
|
}
|
||||||
@@ -135,7 +139,7 @@ func setCanonicalPrepareParty(t *testing.T, env *Env, raw []byte) {
|
|||||||
t.Fatalf("ParseParty() error = %v", err)
|
t.Fatalf("ParseParty() error = %v", err)
|
||||||
}
|
}
|
||||||
env.Config.Party = config.ResolvedParty{
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,19 +177,14 @@ characters:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseParty() error = %v", err)
|
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{
|
env.Config.Party = config.ResolvedParty{
|
||||||
Mode: config.PartyModeCanonical,
|
Canonical: document,
|
||||||
Canonical: document.Canonical,
|
|
||||||
Source: config.PartySource{
|
Source: config.PartySource{
|
||||||
Path: filepath.Join(filepath.Dir(env.Config.CampaignPath), "party.yml"),
|
Path: filepath.Join(filepath.Dir(env.Config.CampaignPath), "party.yml"),
|
||||||
ConfigPath: env.Config.CampaignPath,
|
ConfigPath: env.Config.CampaignPath,
|
||||||
Source: "campaign_config",
|
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")
|
writeFile(t, filepath.Join(filepath.Dir(env.Config.SessionPath), "audio", "alice.flac"), "audio")
|
||||||
|
|
||||||
if _, err := (prepareStage{}).Run(context.Background(), env, m); err != nil {
|
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) {
|
if got, err := os.ReadFile(preparedParty); err != nil || !bytes.Equal(got, partyBytes) {
|
||||||
t.Fatalf("prepared party = %q, %v; want unchanged canonical bytes", got, err)
|
t.Fatalf("prepared party = %q, %v; want unchanged canonical bytes", got, err)
|
||||||
}
|
}
|
||||||
wantPlayers, err := document.Canonical.PlayersYAML()
|
wantPlayers, err := document.PlayersYAML()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("PlayersYAML() error = %v", err)
|
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) {
|
func TestPrepareStageMaterializesSpellCatalogWithProvenance(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -888,15 +859,18 @@ inputs:
|
|||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
players_file: ./players.yml
|
|
||||||
party_file: ./party.yml
|
party_file: ./party.yml
|
||||||
`)
|
`)
|
||||||
writeFile(t, sessionPath, "session_id: 2026-05-03\n")
|
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, "speakers.yml"), "alice: alice.flac\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "players.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"), "[]\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{
|
cfg := &config.Config{
|
||||||
Pipeline: &config.PipelineConfig{
|
Pipeline: &config.PipelineConfig{
|
||||||
@@ -907,6 +881,9 @@ inputs:
|
|||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
CampaignPath: campaignPath,
|
CampaignPath: campaignPath,
|
||||||
PipelinePath: pipelinePath,
|
PipelinePath: pipelinePath,
|
||||||
|
Party: config.ResolvedParty{Canonical: party, Source: config.PartySource{
|
||||||
|
Path: filepath.Join(cfgDir, "party.yml"), ConfigPath: campaignPath, Source: "campaign_config",
|
||||||
|
}},
|
||||||
Session: &config.SessionConfig{
|
Session: &config.SessionConfig{
|
||||||
SessionID: "2026-05-03",
|
SessionID: "2026-05-03",
|
||||||
Campaign: "sample-campaign",
|
Campaign: "sample-campaign",
|
||||||
@@ -930,11 +907,6 @@ inputs:
|
|||||||
ConfigPath: campaignPath,
|
ConfigPath: campaignPath,
|
||||||
Source: "campaign_config",
|
Source: "campaign_config",
|
||||||
},
|
},
|
||||||
PlayersFile: config.ResolvedInputFile{
|
|
||||||
Path: "./players.yml",
|
|
||||||
ConfigPath: campaignPath,
|
|
||||||
Source: "campaign_config",
|
|
||||||
},
|
|
||||||
PartyFile: config.ResolvedInputFile{
|
PartyFile: config.ResolvedInputFile{
|
||||||
Path: "./party.yml",
|
Path: "./party.yml",
|
||||||
ConfigPath: campaignPath,
|
ConfigPath: campaignPath,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type prepareSemanticConfig struct {
|
|||||||
CampaignID string `json:"campaign_id"`
|
CampaignID string `json:"campaign_id"`
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
PreviousSession string `json:"previous_session_id"`
|
PreviousSession string `json:"previous_session_id"`
|
||||||
PartyMode string `json:"party_mode"`
|
|
||||||
PlayersSchema string `json:"players_schema_version,omitempty"`
|
PlayersSchema string `json:"players_schema_version,omitempty"`
|
||||||
Audio prepareAudioSelection `json:"audio"`
|
Audio prepareAudioSelection `json:"audio"`
|
||||||
StableInputs []prepareStableInputSelection `json:"stable_inputs"`
|
StableInputs []prepareStableInputSelection `json:"stable_inputs"`
|
||||||
@@ -106,21 +105,17 @@ func buildPrepareSemanticConfig(env *Env) (prepareSemanticConfig, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return prepareSemanticConfig{}, fmt.Errorf("resolve previous artifact requirements: %w", err)
|
return prepareSemanticConfig{}, fmt.Errorf("resolve previous artifact requirements: %w", err)
|
||||||
}
|
}
|
||||||
partyMode := cfg.Party.Mode
|
if cfg.Party.Canonical == nil {
|
||||||
if partyMode == "" {
|
|
||||||
return prepareSemanticConfig{}, fmt.Errorf("prepare semantic configuration requires a resolved canonical party")
|
return prepareSemanticConfig{}, fmt.Errorf("prepare semantic configuration requires a resolved canonical party")
|
||||||
}
|
}
|
||||||
payload := prepareSemanticConfig{
|
payload := prepareSemanticConfig{
|
||||||
CampaignID: strings.TrimSpace(cfg.Session.Campaign),
|
CampaignID: strings.TrimSpace(cfg.Session.Campaign),
|
||||||
SessionID: strings.TrimSpace(cfg.Session.SessionID),
|
SessionID: strings.TrimSpace(cfg.Session.SessionID),
|
||||||
PreviousSession: strings.TrimSpace(cfg.Session.PreviousSessionID),
|
PreviousSession: strings.TrimSpace(cfg.Session.PreviousSessionID),
|
||||||
PartyMode: string(partyMode),
|
PlayersSchema: config.PlayersSchemaVersion,
|
||||||
Audio: prepareAudioSemantics(cfg),
|
Audio: prepareAudioSemantics(cfg),
|
||||||
StableInputs: prepareStableInputSemantics(cfg),
|
StableInputs: prepareStableInputSemantics(cfg),
|
||||||
}
|
}
|
||||||
if partyMode == config.PartyModeCanonical {
|
|
||||||
payload.PlayersSchema = config.PlayersSchemaVersion
|
|
||||||
}
|
|
||||||
for _, requirement := range requirements {
|
for _, requirement := range requirements {
|
||||||
entry := preparePreviousRequirement{Name: strings.TrimSpace(requirement.Name), Required: requirement.Required}
|
entry := preparePreviousRequirement{Name: strings.TrimSpace(requirement.Name), Required: requirement.Required}
|
||||||
if cfg.Pipeline.Scriptorium != nil {
|
if cfg.Pipeline.Scriptorium != nil {
|
||||||
@@ -159,8 +154,7 @@ func prepareStableInputSemantics(cfg *config.Config) []prepareStableInputSelecti
|
|||||||
prepareStableInput("speakers", stable.SpeakersFile, cfg.Session.Inputs.SpeakersFile),
|
prepareStableInput("speakers", stable.SpeakersFile, cfg.Session.Inputs.SpeakersFile),
|
||||||
prepareStableInput("autocorrect", stable.AutocorrectFile, cfg.Session.Inputs.AutocorrectFile),
|
prepareStableInput("autocorrect", stable.AutocorrectFile, cfg.Session.Inputs.AutocorrectFile),
|
||||||
prepareStableInput("glossary", stable.GlossaryFile, cfg.Session.Inputs.GlossaryFile),
|
prepareStableInput("glossary", stable.GlossaryFile, cfg.Session.Inputs.GlossaryFile),
|
||||||
prepareStableInput("players", stable.PlayersFile, cfg.Session.Inputs.PlayersFile),
|
prepareStableInput("party", stable.PartyFile, ""),
|
||||||
prepareStableInput("party", stable.PartyFile, cfg.Session.Inputs.PartyFile),
|
|
||||||
prepareStableInput("spell_catalog", stable.SpellCatalogFile, cfg.Session.Inputs.SpellCatalogFile),
|
prepareStableInput("spell_catalog", stable.SpellCatalogFile, cfg.Session.Inputs.SpellCatalogFile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,11 @@ func TestPrepareSemanticConfigSensitivity(t *testing.T) {
|
|||||||
|
|
||||||
func TestPrepareSemanticConfigRecordsCanonicalProjectionVersion(t *testing.T) {
|
func TestPrepareSemanticConfigRecordsCanonicalProjectionVersion(t *testing.T) {
|
||||||
env := initialSemanticEnv()
|
env := initialSemanticEnv()
|
||||||
env.Config.Party.Mode = config.PartyModeCanonical
|
|
||||||
payload, err := buildPrepareSemanticConfig(env)
|
payload, err := buildPrepareSemanticConfig(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("buildPrepareSemanticConfig() error = %v", err)
|
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)
|
t.Fatalf("prepare semantic payload = %#v", payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,13 +119,13 @@ func initialSemanticEnv() *Env {
|
|||||||
Session: &config.SessionConfig{
|
Session: &config.SessionConfig{
|
||||||
SessionID: "2026-05-03", Campaign: "campaign",
|
SessionID: "2026-05-03", Campaign: "campaign",
|
||||||
Inputs: config.SessionInputsConfig{
|
Inputs: config.SessionInputsConfig{
|
||||||
AudioFiles: []string{"/audio/speaker.flac"}, PartyFile: "party.yml",
|
AudioFiles: []string{"/audio/speaker.flac"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
StableInputs: config.ResolvedStableInputs{
|
StableInputs: config.ResolvedStableInputs{
|
||||||
PartyFile: config.ResolvedInputFile{Path: "/campaign/party.yml", ConfigPath: "/campaign/campaign.yml", Source: "session_config"},
|
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{}},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -401,12 +401,16 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
|||||||
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||||
writeFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\n")
|
writeFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\n")
|
||||||
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\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, "speakers.yml"), "alice: alice.flac\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "players.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"), "[]\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
|
retries := 3
|
||||||
concurrency := 2
|
concurrency := 2
|
||||||
@@ -415,6 +419,9 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
|||||||
CampaignPath: campaignPath,
|
CampaignPath: campaignPath,
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
Campaign: &config.CampaignConfig{CampaignID: "sample-campaign"},
|
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{
|
Pipeline: &config.PipelineConfig{
|
||||||
Workspace: config.WorkspaceConfig{Root: workspace},
|
Workspace: config.WorkspaceConfig{Root: workspace},
|
||||||
WhisperX: config.WhisperXConfig{
|
WhisperX: config.WhisperXConfig{
|
||||||
@@ -434,15 +441,12 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
|||||||
SpeakersFile: "./speakers.yml",
|
SpeakersFile: "./speakers.yml",
|
||||||
AutocorrectFile: "./autocorrect.yml",
|
AutocorrectFile: "./autocorrect.yml",
|
||||||
GlossaryFile: "./glossary.yml",
|
GlossaryFile: "./glossary.yml",
|
||||||
PlayersFile: "./players.yml",
|
|
||||||
PartyFile: "./party.yml",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
StableInputs: config.ResolvedStableInputs{
|
StableInputs: config.ResolvedStableInputs{
|
||||||
SpeakersFile: config.ResolvedInputFile{Path: "./speakers.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
SpeakersFile: config.ResolvedInputFile{Path: "./speakers.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||||
AutocorrectFile: config.ResolvedInputFile{Path: "./autocorrect.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"},
|
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"},
|
PartyFile: config.ResolvedInputFile{Path: "./party.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user