Add Notarius reference configuration vocabulary
This commit is contained in:
@@ -16,7 +16,7 @@ different contract.
|
|||||||
|
|
||||||
| Stage | Outcome | Status |
|
| Stage | Outcome | Status |
|
||||||
| ---: | --- | --- |
|
| ---: | --- | --- |
|
||||||
| 1 | Add the reference-selector and configuration vocabulary, including optional spell-catalog inputs. | Pending |
|
| 1 | Add the reference-selector and configuration vocabulary, including optional spell-catalog inputs. | Completed |
|
||||||
| 2 | Materialize and inventory the optional spell catalog through the prepare and operator lifecycle. | Pending |
|
| 2 | Materialize and inventory the optional spell catalog through the prepare and operator lifecycle. | Pending |
|
||||||
| 3 | Centralize manifest-authoritative prepared-input resolution and migrate analyze to it. | Pending |
|
| 3 | Centralize manifest-authoritative prepared-input resolution and migrate analyze to it. | Pending |
|
||||||
| 4 | Add deterministic Notarius v0.6 reference arguments at the subprocess adapter boundary. | Pending |
|
| 4 | Add deterministic Notarius v0.6 reference arguments at the subprocess adapter boundary. | Pending |
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import (
|
|||||||
const (
|
const (
|
||||||
SourceBoundsSession = "narratio.bounds.session"
|
SourceBoundsSession = "narratio.bounds.session"
|
||||||
|
|
||||||
SourceInputPlayers = "narratio.input.players"
|
SourceInputPlayers = "narratio.input.players"
|
||||||
SourceInputParty = "narratio.input.party"
|
SourceInputParty = "narratio.input.party"
|
||||||
SourceInputGlossary = "narratio.input.glossary"
|
SourceInputGlossary = "narratio.input.glossary"
|
||||||
|
SourceInputSpellCatalog = "narratio.input.spell_catalog"
|
||||||
|
|
||||||
configuredSourcePrefix = "narratio.artifact."
|
configuredSourcePrefix = "narratio.artifact."
|
||||||
extractionSourcePrefix = "narratio.extraction."
|
extractionSourcePrefix = "narratio.extraction."
|
||||||
@@ -52,9 +53,18 @@ type Source struct {
|
|||||||
// ScriptoriumInputSourceDescriptor describes one validated Scriptorium input source.
|
// ScriptoriumInputSourceDescriptor describes one validated Scriptorium input source.
|
||||||
type ScriptoriumInputSourceDescriptor struct {
|
type ScriptoriumInputSourceDescriptor struct {
|
||||||
Source Source
|
Source Source
|
||||||
|
PreparedInput *PreparedInputSourceDescriptor
|
||||||
PreviousSession *PreviousSessionSourceDescriptor
|
PreviousSession *PreviousSessionSourceDescriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreparedInputSourceDescriptor describes a prepared stable input's source,
|
||||||
|
// manifest kind, and canonical staged filename.
|
||||||
|
type PreparedInputSourceDescriptor struct {
|
||||||
|
SourceID string
|
||||||
|
ManifestKind string
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
// PreviousSessionSourceDescriptor describes one canonical previous-session input source.
|
// PreviousSessionSourceDescriptor describes one canonical previous-session input source.
|
||||||
type PreviousSessionSourceDescriptor struct {
|
type PreviousSessionSourceDescriptor struct {
|
||||||
SourceID string
|
SourceID string
|
||||||
@@ -158,9 +168,10 @@ func DescribeScriptoriumInputSource(source string) (ScriptoriumInputSourceDescri
|
|||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
return ScriptoriumInputSourceDescriptor{}, ErrUnsupportedScriptoriumInputSource
|
return ScriptoriumInputSourceDescriptor{}, ErrUnsupportedScriptoriumInputSource
|
||||||
}
|
}
|
||||||
if IsStableInputSource(trimmed) {
|
if prepared, ok := DescribePreparedInputSource(trimmed); ok {
|
||||||
return ScriptoriumInputSourceDescriptor{
|
return ScriptoriumInputSourceDescriptor{
|
||||||
Source: Source{ID: trimmed, Kind: SourceKindStableInput},
|
Source: Source{ID: prepared.SourceID, Kind: SourceKindStableInput},
|
||||||
|
PreparedInput: &prepared,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(trimmed, "narratio.previous_session.artifact") {
|
if strings.HasPrefix(trimmed, "narratio.previous_session.artifact") {
|
||||||
@@ -188,12 +199,38 @@ func DescribeScriptoriumInputSource(source string) (ScriptoriumInputSourceDescri
|
|||||||
// IsStableInputSource reports whether source is a prepared stable input source
|
// IsStableInputSource reports whether source is a prepared stable input source
|
||||||
// available only to Scriptorium input resolution.
|
// available only to Scriptorium input resolution.
|
||||||
func IsStableInputSource(source string) bool {
|
func IsStableInputSource(source string) bool {
|
||||||
switch strings.TrimSpace(source) {
|
_, ok := DescribePreparedInputSource(source)
|
||||||
case SourceInputPlayers, SourceInputParty, SourceInputGlossary:
|
return ok
|
||||||
return true
|
}
|
||||||
default:
|
|
||||||
return false
|
var preparedInputSources = map[string]PreparedInputSourceDescriptor{
|
||||||
}
|
SourceInputPlayers: {
|
||||||
|
SourceID: SourceInputPlayers,
|
||||||
|
ManifestKind: "players",
|
||||||
|
Filename: "players.yml",
|
||||||
|
},
|
||||||
|
SourceInputParty: {
|
||||||
|
SourceID: SourceInputParty,
|
||||||
|
ManifestKind: "party",
|
||||||
|
Filename: "party.yml",
|
||||||
|
},
|
||||||
|
SourceInputGlossary: {
|
||||||
|
SourceID: SourceInputGlossary,
|
||||||
|
ManifestKind: "glossary",
|
||||||
|
Filename: "glossary.yml",
|
||||||
|
},
|
||||||
|
SourceInputSpellCatalog: {
|
||||||
|
SourceID: SourceInputSpellCatalog,
|
||||||
|
ManifestKind: "spell_catalog",
|
||||||
|
Filename: "spell_catalog.json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// DescribePreparedInputSource returns the canonical descriptor for a prepared
|
||||||
|
// stable input source.
|
||||||
|
func DescribePreparedInputSource(source string) (PreparedInputSourceDescriptor, bool) {
|
||||||
|
descriptor, ok := preparedInputSources[strings.TrimSpace(source)]
|
||||||
|
return descriptor, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// DescribePreviousSessionSource validates a canonical previous-session source id
|
// DescribePreviousSessionSource validates a canonical previous-session source id
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ func TestDescribeScriptoriumInputSource(t *testing.T) {
|
|||||||
{name: "prepared players input", source: "narratio.input.players", wantKind: SourceKindStableInput},
|
{name: "prepared players input", source: "narratio.input.players", wantKind: SourceKindStableInput},
|
||||||
{name: "prepared party input", source: "narratio.input.party", wantKind: SourceKindStableInput},
|
{name: "prepared party input", source: "narratio.input.party", wantKind: SourceKindStableInput},
|
||||||
{name: "prepared glossary input", source: "narratio.input.glossary", wantKind: SourceKindStableInput},
|
{name: "prepared glossary input", source: "narratio.input.glossary", wantKind: SourceKindStableInput},
|
||||||
|
{name: "prepared spell catalog input", source: "narratio.input.spell_catalog", wantKind: SourceKindStableInput},
|
||||||
{name: "configured", source: "narratio.artifact.session_recap", wantKind: SourceKindConfiguredArtifact, wantKey: "session_recap"},
|
{name: "configured", source: "narratio.artifact.session_recap", wantKind: SourceKindConfiguredArtifact, wantKey: "session_recap"},
|
||||||
{name: "previous", source: "narratio.previous_session.artifact.session_recap", wantKind: SourceKindPreviousArtifact, wantKey: "session_recap", wantPrev: true},
|
{name: "previous", source: "narratio.previous_session.artifact.session_recap", wantKind: SourceKindPreviousArtifact, wantKey: "session_recap", wantPrev: true},
|
||||||
{name: "invalid previous", source: "narratio.previous_session.artifact.", wantErr: ErrInvalidPreviousSessionSource},
|
{name: "invalid previous", source: "narratio.previous_session.artifact.", wantErr: ErrInvalidPreviousSessionSource},
|
||||||
@@ -211,6 +212,43 @@ func TestDescribeScriptoriumInputSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDescribePreparedInputSource(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
source string
|
||||||
|
manifestKind string
|
||||||
|
filename string
|
||||||
|
}{
|
||||||
|
{source: SourceInputPlayers, manifestKind: "players", filename: "players.yml"},
|
||||||
|
{source: SourceInputParty, manifestKind: "party", filename: "party.yml"},
|
||||||
|
{source: SourceInputGlossary, manifestKind: "glossary", filename: "glossary.yml"},
|
||||||
|
{source: SourceInputSpellCatalog, manifestKind: "spell_catalog", filename: "spell_catalog.json"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.manifestKind, func(t *testing.T) {
|
||||||
|
descriptor, ok := DescribePreparedInputSource(" " + tt.source + " ")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("DescribePreparedInputSource(%q) ok = false", tt.source)
|
||||||
|
}
|
||||||
|
if descriptor.SourceID != tt.source || descriptor.ManifestKind != tt.manifestKind || descriptor.Filename != tt.filename {
|
||||||
|
t.Fatalf("DescribePreparedInputSource(%q) = %#v", tt.source, descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptoriumDescriptor, err := DescribeScriptoriumInputSource(tt.source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DescribeScriptoriumInputSource(%q) error = %v", tt.source, err)
|
||||||
|
}
|
||||||
|
if scriptoriumDescriptor.PreparedInput == nil || *scriptoriumDescriptor.PreparedInput != descriptor {
|
||||||
|
t.Fatalf("prepared input descriptor = %#v, want %#v", scriptoriumDescriptor.PreparedInput, descriptor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := DescribePreparedInputSource("narratio.input.unknown"); ok {
|
||||||
|
t.Fatal("DescribePreparedInputSource(unknown) ok = true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateInputConfiguredReference(t *testing.T) {
|
func TestValidateInputConfiguredReference(t *testing.T) {
|
||||||
configured := map[string]struct{}{"session_recap": {}}
|
configured := map[string]struct{}{"session_recap": {}}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,74 @@ func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
|||||||
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./session-party.yml", sessionPath, "session_config")
|
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./session-party.yml", sessionPath, "session_config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCampaignSessionMergeSpellCatalog(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
campaignValue string
|
||||||
|
sessionValue string
|
||||||
|
wantPath string
|
||||||
|
wantSource string
|
||||||
|
}{
|
||||||
|
{name: "omitted", wantPath: "", wantSource: "campaign_config"},
|
||||||
|
{name: "campaign inherited", campaignValue: "./campaign-spells.json", wantPath: "./campaign-spells.json", wantSource: "campaign_config"},
|
||||||
|
{name: "session override", campaignValue: "./campaign-spells.json", sessionValue: "./session-spells.json", wantPath: "./session-spells.json", wantSource: "session_config"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
campaignSpell := ""
|
||||||
|
if tt.campaignValue != "" {
|
||||||
|
campaignSpell = " spell_catalog_file: " + tt.campaignValue + "\n"
|
||||||
|
}
|
||||||
|
sessionSpell := ""
|
||||||
|
if tt.sessionValue != "" {
|
||||||
|
sessionSpell = " spell_catalog_file: " + tt.sessionValue + "\n"
|
||||||
|
}
|
||||||
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||||
|
"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"+campaignSpell,
|
||||||
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n"+sessionSpell,
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||||
|
}
|
||||||
|
wantConfigPath := campaignPath
|
||||||
|
if tt.wantSource == "session_config" {
|
||||||
|
wantConfigPath = sessionPath
|
||||||
|
}
|
||||||
|
assertResolvedStableInput(t, cfg.StableInputs.SpellCatalogFile, tt.wantPath, wantConfigPath, tt.wantSource)
|
||||||
|
if cfg.Session.Inputs.SpellCatalogFile != tt.wantPath {
|
||||||
|
t.Fatalf("session spell_catalog_file = %q, want %q", cfg.Session.Inputs.SpellCatalogFile, tt.wantPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCampaignSessionMergeRejectsWhitespaceSpellCatalog(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
campaignLine string
|
||||||
|
sessionLine string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "campaign", campaignLine: " spell_catalog_file: ' '\n", wantErr: "campaign.inputs.spell_catalog_file"},
|
||||||
|
{name: "session", sessionLine: " spell_catalog_file: ' '\n", wantErr: "session.inputs.spell_catalog_file"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||||
|
"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"+tt.campaignLine,
|
||||||
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n"+tt.sessionLine,
|
||||||
|
)
|
||||||
|
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("LoadWithSessionOptions() error = %v, want containing %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCampaignRequiresPlayersAndPartyInputs(t *testing.T) {
|
func TestCampaignRequiresPlayersAndPartyInputs(t *testing.T) {
|
||||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||||
|
|||||||
@@ -49,11 +49,12 @@ type CampaignConfig struct {
|
|||||||
|
|
||||||
// CampaignInputsConfig contains stable campaign-level input file references.
|
// CampaignInputsConfig contains stable campaign-level input file references.
|
||||||
type CampaignInputsConfig struct {
|
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"`
|
PlayersFile string `yaml:"players_file"`
|
||||||
PartyFile string `yaml:"party_file"`
|
PartyFile string `yaml:"party_file"`
|
||||||
|
SpellCatalogFile string `yaml:"spell_catalog_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionConfig contains per-session inputs and metadata.
|
// SessionConfig contains per-session inputs and metadata.
|
||||||
@@ -266,6 +267,7 @@ type NotariusConfig struct {
|
|||||||
PipelineID string `yaml:"pipeline_id"`
|
PipelineID string `yaml:"pipeline_id"`
|
||||||
Timeout string `yaml:"timeout"`
|
Timeout string `yaml:"timeout"`
|
||||||
WorkingDirectory string `yaml:"working_directory"`
|
WorkingDirectory string `yaml:"working_directory"`
|
||||||
|
References map[string]string `yaml:"references"`
|
||||||
Outputs map[string]NotariusOutputConfig `yaml:"outputs"`
|
Outputs map[string]NotariusOutputConfig `yaml:"outputs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,14 +287,15 @@ type NotificationConfig struct {
|
|||||||
|
|
||||||
// SessionInputsConfig contains per-session input references.
|
// SessionInputsConfig contains per-session input references.
|
||||||
type SessionInputsConfig struct {
|
type SessionInputsConfig struct {
|
||||||
AudioDir string `yaml:"audio_dir"`
|
AudioDir string `yaml:"audio_dir"`
|
||||||
AudioFiles []string `yaml:"audio_files"`
|
AudioFiles []string `yaml:"audio_files"`
|
||||||
AudioS3 *SessionAudioS3Input `yaml:"audio_s3"`
|
AudioS3 *SessionAudioS3Input `yaml:"audio_s3"`
|
||||||
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"`
|
PlayersFile string `yaml:"players_file"`
|
||||||
PartyFile string `yaml:"party_file"`
|
PartyFile string `yaml:"party_file"`
|
||||||
|
SpellCatalogFile string `yaml:"spell_catalog_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionAudioS3Input configures S3 session-audio input discovery.
|
// SessionAudioS3Input configures S3 session-audio input discovery.
|
||||||
@@ -303,11 +306,12 @@ type SessionAudioS3Input struct {
|
|||||||
// ResolvedStableInputs records where stable input file paths came from after
|
// ResolvedStableInputs records where stable input file paths came from after
|
||||||
// campaign/session merge.
|
// campaign/session merge.
|
||||||
type ResolvedStableInputs struct {
|
type ResolvedStableInputs struct {
|
||||||
SpeakersFile ResolvedInputFile
|
SpeakersFile ResolvedInputFile
|
||||||
AutocorrectFile ResolvedInputFile
|
AutocorrectFile ResolvedInputFile
|
||||||
GlossaryFile ResolvedInputFile
|
GlossaryFile ResolvedInputFile
|
||||||
PlayersFile ResolvedInputFile
|
PlayersFile ResolvedInputFile
|
||||||
PartyFile ResolvedInputFile
|
PartyFile ResolvedInputFile
|
||||||
|
SpellCatalogFile ResolvedInputFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedInputFile records one merged config path and its source config file.
|
// ResolvedInputFile records one merged config path and its source config file.
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ const (
|
|||||||
DefaultAuditaReport = true
|
DefaultAuditaReport = true
|
||||||
DefaultNotariusBinary = "notarius"
|
DefaultNotariusBinary = "notarius"
|
||||||
DefaultNotariusTimeout = "3h"
|
DefaultNotariusTimeout = "3h"
|
||||||
|
// MaxNotariusReferenceBindings is the maximum number of CLI reference
|
||||||
|
// bindings accepted for one Notarius invocation.
|
||||||
|
MaxNotariusReferenceBindings = 256
|
||||||
|
|
||||||
DefaultScriptoriumBinary = "scriptorium"
|
DefaultScriptoriumBinary = "scriptorium"
|
||||||
DefaultScriptoriumTimeout = "10m"
|
DefaultScriptoriumTimeout = "10m"
|
||||||
|
|||||||
@@ -209,6 +209,12 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
|
|||||||
if sessionCfg == nil {
|
if sessionCfg == nil {
|
||||||
return ResolvedStableInputs{}, fmt.Errorf("session config is required")
|
return ResolvedStableInputs{}, fmt.Errorf("session config is required")
|
||||||
}
|
}
|
||||||
|
if campaignCfg.Inputs.SpellCatalogFile != "" && strings.TrimSpace(campaignCfg.Inputs.SpellCatalogFile) == "" {
|
||||||
|
return ResolvedStableInputs{}, fmt.Errorf("campaign.inputs.spell_catalog_file must be non-empty when provided")
|
||||||
|
}
|
||||||
|
if sessionCfg.Inputs.SpellCatalogFile != "" && strings.TrimSpace(sessionCfg.Inputs.SpellCatalogFile) == "" {
|
||||||
|
return ResolvedStableInputs{}, fmt.Errorf("session.inputs.spell_catalog_file must be non-empty when provided")
|
||||||
|
}
|
||||||
|
|
||||||
campaignName := CampaignID(campaignCfg)
|
campaignName := CampaignID(campaignCfg)
|
||||||
sessionCampaign := strings.TrimSpace(sessionCfg.Campaign)
|
sessionCampaign := strings.TrimSpace(sessionCfg.Campaign)
|
||||||
@@ -254,6 +260,12 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
|
|||||||
campaignPath,
|
campaignPath,
|
||||||
sessionPath,
|
sessionPath,
|
||||||
),
|
),
|
||||||
|
SpellCatalogFile: selectStableInput(
|
||||||
|
campaignCfg.Inputs.SpellCatalogFile,
|
||||||
|
sessionCfg.Inputs.SpellCatalogFile,
|
||||||
|
campaignPath,
|
||||||
|
sessionPath,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
||||||
@@ -261,6 +273,7 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
|
|||||||
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.Path
|
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.Path
|
||||||
sessionCfg.Inputs.PlayersFile = stable.PlayersFile.Path
|
sessionCfg.Inputs.PlayersFile = stable.PlayersFile.Path
|
||||||
sessionCfg.Inputs.PartyFile = stable.PartyFile.Path
|
sessionCfg.Inputs.PartyFile = stable.PartyFile.Path
|
||||||
|
sessionCfg.Inputs.SpellCatalogFile = stable.SpellCatalogFile.Path
|
||||||
return stable, nil
|
return stable, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -108,6 +109,7 @@ func TestNotariusStrictYAML(t *testing.T) {
|
|||||||
{name: "unknown output field", yaml: "notarius:\n outputs:\n npc_registry:\n lane_id: npc-registry\n unknown: true\n"},
|
{name: "unknown output field", yaml: "notarius:\n outputs:\n npc_registry:\n lane_id: npc-registry\n unknown: true\n"},
|
||||||
{name: "unsupported session id", yaml: "notarius:\n session_id: forbidden\n"},
|
{name: "unsupported session id", yaml: "notarius:\n session_id: forbidden\n"},
|
||||||
{name: "unsupported model", yaml: "notarius:\n model: forbidden\n"},
|
{name: "unsupported model", yaml: "notarius:\n model: forbidden\n"},
|
||||||
|
{name: "duplicate reference selector", yaml: "notarius:\n references:\n party: narratio.input.party\n party: narratio.input.players\n"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -122,6 +124,134 @@ func TestNotariusStrictYAML(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNotariusReferenceValidationAndNormalization(t *testing.T) {
|
||||||
|
cfg := validNotariusConfig()
|
||||||
|
cfg.References = map[string]string{
|
||||||
|
" party ": " narratio.input.party ",
|
||||||
|
" chunk . players ": "narratio.input.players",
|
||||||
|
" npc-registry . extract . glossary ": "narratio.input.glossary",
|
||||||
|
"spells": "narratio.input.spell_catalog",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateNotarius(cfg, nil); err != nil {
|
||||||
|
t.Fatalf("validateNotarius() error = %v", err)
|
||||||
|
}
|
||||||
|
want := map[string]string{
|
||||||
|
"party": "narratio.input.party",
|
||||||
|
"chunk.players": "narratio.input.players",
|
||||||
|
"npc-registry.extract.glossary": "narratio.input.glossary",
|
||||||
|
"spells": "narratio.input.spell_catalog",
|
||||||
|
}
|
||||||
|
if len(cfg.References) != len(want) {
|
||||||
|
t.Fatalf("normalized references = %#v, want %#v", cfg.References, want)
|
||||||
|
}
|
||||||
|
for selector, source := range want {
|
||||||
|
if cfg.References[selector] != source {
|
||||||
|
t.Fatalf("references[%q] = %q, want %q", selector, cfg.References[selector], source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotariusReferenceValidationRejectsInvalidBindings(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
references map[string]string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "empty selector", references: map[string]string{" ": "narratio.input.party"}, wantErr: "selector is required"},
|
||||||
|
{name: "equals in selector", references: map[string]string{"party=x": "narratio.input.party"}, wantErr: "must not contain"},
|
||||||
|
{name: "invalid stage", references: map[string]string{"lane.prepare.party": "narratio.input.party"}, wantErr: "middle component"},
|
||||||
|
{name: "empty source", references: map[string]string{"party": " "}, wantErr: "source is required"},
|
||||||
|
{name: "unsupported source", references: map[string]string{"party": "narratio.input.unknown"}, wantErr: "not a supported prepared input source"},
|
||||||
|
{
|
||||||
|
name: "normalized collision",
|
||||||
|
references: map[string]string{
|
||||||
|
"chunk.party": "narratio.input.party",
|
||||||
|
" chunk . party ": "narratio.input.players",
|
||||||
|
},
|
||||||
|
wantErr: "normalize to",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := validNotariusConfig()
|
||||||
|
cfg.References = tt.references
|
||||||
|
err := validateNotarius(cfg, nil)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("validateNotarius() error = %v, want containing %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotariusReferenceLimit(t *testing.T) {
|
||||||
|
for _, count := range []int{MaxNotariusReferenceBindings, MaxNotariusReferenceBindings + 1} {
|
||||||
|
t.Run(fmt.Sprintf("count_%d", count), func(t *testing.T) {
|
||||||
|
cfg := validNotariusConfig()
|
||||||
|
cfg.References = make(map[string]string, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
cfg.References[fmt.Sprintf("lane-%03d.party", i)] = "narratio.input.party"
|
||||||
|
}
|
||||||
|
err := validateNotarius(cfg, nil)
|
||||||
|
if count == MaxNotariusReferenceBindings {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("validateNotarius() at limit error = %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "at most 256 bindings") {
|
||||||
|
t.Fatalf("validateNotarius() above limit error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotariusNilAndEmptyReferencesAreValid(t *testing.T) {
|
||||||
|
for _, references := range []map[string]string{nil, {}} {
|
||||||
|
cfg := validNotariusConfig()
|
||||||
|
cfg.References = references
|
||||||
|
if err := validateNotarius(cfg, nil); err != nil {
|
||||||
|
t.Fatalf("validateNotarius(%#v) error = %v", references, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotariusSpellCatalogReferenceRequiresEffectiveInput(t *testing.T) {
|
||||||
|
cfg := loadedValidConfig(t)
|
||||||
|
cfg.Pipeline.Notarius = validNotariusConfig()
|
||||||
|
cfg.Pipeline.Notarius.References = map[string]string{"spells": "narratio.input.spell_catalog"}
|
||||||
|
|
||||||
|
err := Validate(cfg)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "requires campaign.inputs.spell_catalog_file or session.inputs.spell_catalog_file") {
|
||||||
|
t.Fatalf("Validate() error = %v, want missing spell catalog input", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.StableInputs.SpellCatalogFile = ResolvedInputFile{Path: "./spells.json", Source: "campaign_config"}
|
||||||
|
if err := Validate(cfg); err != nil {
|
||||||
|
t.Fatalf("Validate() with spell catalog error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validNotariusConfig() *NotariusConfig {
|
||||||
|
return &NotariusConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Binary: "notarius",
|
||||||
|
ConfigPath: "./notarius.yml",
|
||||||
|
PipelineID: "dnd-session",
|
||||||
|
Timeout: "45m",
|
||||||
|
WorkingDirectory: ".",
|
||||||
|
Outputs: map[string]NotariusOutputConfig{
|
||||||
|
"npc_registry": {
|
||||||
|
LaneID: "npc-registry",
|
||||||
|
MediaType: "application/json",
|
||||||
|
SchemaID: "notarius.dnd.npc_registry",
|
||||||
|
SchemaVersion: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNotariusEnabledValidation(t *testing.T) {
|
func TestNotariusEnabledValidation(t *testing.T) {
|
||||||
valid := `notarius:
|
valid := `notarius:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/notariusref"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ func Validate(cfg *Config) error {
|
|||||||
if err := validateSession(cfg.Session); err != nil {
|
if err := validateSession(cfg.Session); err != nil {
|
||||||
return fmt.Errorf("session config %q invalid: %w", shortName(cfg.SessionPath, "session.yml"), err)
|
return fmt.Errorf("session config %q invalid: %w", shortName(cfg.SessionPath, "session.yml"), err)
|
||||||
}
|
}
|
||||||
if err := validateCrossConfig(cfg.Pipeline, cfg.Session); err != nil {
|
if err := validateCrossConfig(cfg.Pipeline, cfg.Session, cfg.StableInputs); err != nil {
|
||||||
return fmt.Errorf("pipeline/session config invalid: %w", err)
|
return fmt.Errorf("pipeline/session config invalid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +72,9 @@ func validateCampaign(cfg *CampaignConfig) error {
|
|||||||
if strings.TrimSpace(cfg.Inputs.PartyFile) == "" {
|
if strings.TrimSpace(cfg.Inputs.PartyFile) == "" {
|
||||||
return fmt.Errorf("campaign.inputs.party_file is required")
|
return fmt.Errorf("campaign.inputs.party_file is required")
|
||||||
}
|
}
|
||||||
|
if cfg.Inputs.SpellCatalogFile != "" && strings.TrimSpace(cfg.Inputs.SpellCatalogFile) == "" {
|
||||||
|
return fmt.Errorf("campaign.inputs.spell_catalog_file must be non-empty when provided")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +551,39 @@ func validateNotarius(cfg *NotariusConfig, scriptorium *ScriptoriumConfig) error
|
|||||||
if strings.TrimSpace(cfg.WorkingDirectory) == "" {
|
if strings.TrimSpace(cfg.WorkingDirectory) == "" {
|
||||||
return fmt.Errorf("pipeline.notarius.working_directory is required when pipeline.notarius.enabled is true")
|
return fmt.Errorf("pipeline.notarius.working_directory is required when pipeline.notarius.enabled is true")
|
||||||
}
|
}
|
||||||
|
if len(cfg.References) > MaxNotariusReferenceBindings {
|
||||||
|
return fmt.Errorf("pipeline.notarius.references must contain at most %d bindings", MaxNotariusReferenceBindings)
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceKeys := make([]string, 0, len(cfg.References))
|
||||||
|
for selector := range cfg.References {
|
||||||
|
referenceKeys = append(referenceKeys, selector)
|
||||||
|
}
|
||||||
|
sort.Strings(referenceKeys)
|
||||||
|
var normalizedReferences map[string]string
|
||||||
|
if cfg.References != nil {
|
||||||
|
normalizedReferences = make(map[string]string, len(cfg.References))
|
||||||
|
}
|
||||||
|
referenceOwners := make(map[string]string, len(cfg.References))
|
||||||
|
for _, rawSelector := range referenceKeys {
|
||||||
|
selector, err := notariusref.NormalizeSelector(rawSelector)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pipeline.notarius.references selector %q is invalid: %w", rawSelector, err)
|
||||||
|
}
|
||||||
|
if previous, ok := referenceOwners[selector]; ok {
|
||||||
|
return fmt.Errorf("pipeline.notarius.references selectors %q and %q normalize to %q", previous, rawSelector, selector)
|
||||||
|
}
|
||||||
|
referenceOwners[selector] = rawSelector
|
||||||
|
|
||||||
|
source := strings.TrimSpace(cfg.References[rawSelector])
|
||||||
|
if source == "" {
|
||||||
|
return fmt.Errorf("pipeline.notarius.references.%s source is required", selector)
|
||||||
|
}
|
||||||
|
if _, ok := artifactpolicy.DescribePreparedInputSource(source); !ok {
|
||||||
|
return fmt.Errorf("pipeline.notarius.references.%s source %q is not a supported prepared input source", selector, source)
|
||||||
|
}
|
||||||
|
normalizedReferences[selector] = source
|
||||||
|
}
|
||||||
|
|
||||||
reservedSources := map[string]string{}
|
reservedSources := map[string]string{}
|
||||||
for _, spec := range artifactmodel.RuntimeTranscriptArtifacts() {
|
for _, spec := range artifactmodel.RuntimeTranscriptArtifacts() {
|
||||||
@@ -613,6 +650,7 @@ func validateNotarius(cfg *NotariusConfig, scriptorium *ScriptoriumConfig) error
|
|||||||
cfg.PipelineID = strings.TrimSpace(cfg.PipelineID)
|
cfg.PipelineID = strings.TrimSpace(cfg.PipelineID)
|
||||||
cfg.Timeout = strings.TrimSpace(cfg.Timeout)
|
cfg.Timeout = strings.TrimSpace(cfg.Timeout)
|
||||||
cfg.WorkingDirectory = filepath.Clean(cfg.WorkingDirectory)
|
cfg.WorkingDirectory = filepath.Clean(cfg.WorkingDirectory)
|
||||||
|
cfg.References = normalizedReferences
|
||||||
cfg.Outputs = normalizedOutputs
|
cfg.Outputs = normalizedOutputs
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -762,6 +800,9 @@ func validateSession(cfg *SessionConfig) error {
|
|||||||
if strings.TrimSpace(cfg.Inputs.PartyFile) == "" {
|
if strings.TrimSpace(cfg.Inputs.PartyFile) == "" {
|
||||||
return fmt.Errorf("session.inputs.party_file is required")
|
return fmt.Errorf("session.inputs.party_file is required")
|
||||||
}
|
}
|
||||||
|
if cfg.Inputs.SpellCatalogFile != "" && strings.TrimSpace(cfg.Inputs.SpellCatalogFile) == "" {
|
||||||
|
return fmt.Errorf("session.inputs.spell_catalog_file must be non-empty when provided")
|
||||||
|
}
|
||||||
|
|
||||||
hasAudioDir := strings.TrimSpace(cfg.Inputs.AudioDir) != ""
|
hasAudioDir := strings.TrimSpace(cfg.Inputs.AudioDir) != ""
|
||||||
hasAudioFiles := len(cfg.Inputs.AudioFiles) > 0
|
hasAudioFiles := len(cfg.Inputs.AudioFiles) > 0
|
||||||
@@ -797,10 +838,17 @@ func validateSessionIdentifier(fieldName, value string, required bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCrossConfig(pipeline *PipelineConfig, session *SessionConfig) error {
|
func validateCrossConfig(pipeline *PipelineConfig, session *SessionConfig, stableInputs ResolvedStableInputs) error {
|
||||||
if pipeline == nil || session == nil {
|
if pipeline == nil || session == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if pipeline.Notarius != nil && pipeline.Notarius.Enabled {
|
||||||
|
for selector, source := range pipeline.Notarius.References {
|
||||||
|
if source == artifactpolicy.SourceInputSpellCatalog && strings.TrimSpace(stableInputs.SpellCatalogFile.Path) == "" {
|
||||||
|
return fmt.Errorf("pipeline.notarius.references.%s requires campaign.inputs.spell_catalog_file or session.inputs.spell_catalog_file", selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
audioS3Enabled := session.Inputs.AudioS3 != nil
|
audioS3Enabled := session.Inputs.AudioS3 != nil
|
||||||
publishUploadEnabled := publishUploadConfiguredForS3(pipeline)
|
publishUploadEnabled := publishUploadConfiguredForS3(pipeline)
|
||||||
|
|||||||
43
internal/notariusref/selector.go
Normal file
43
internal/notariusref/selector.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Package notariusref owns Narratio's Notarius CLI reference-selector
|
||||||
|
// vocabulary without depending on Notarius implementation packages.
|
||||||
|
package notariusref
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NormalizeSelector validates and normalizes a Notarius v0.6 reference
|
||||||
|
// selector. It intentionally validates selector structure only; Notarius owns
|
||||||
|
// target, slot, and media compatibility.
|
||||||
|
func NormalizeSelector(value string) (string, error) {
|
||||||
|
selector := strings.TrimSpace(value)
|
||||||
|
if selector == "" {
|
||||||
|
return "", fmt.Errorf("reference selector is required")
|
||||||
|
}
|
||||||
|
if strings.Contains(selector, "=") {
|
||||||
|
return "", fmt.Errorf("reference selector must not contain '='")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(selector, ".")
|
||||||
|
for index := range parts {
|
||||||
|
parts[index] = strings.TrimSpace(parts[index])
|
||||||
|
if parts[index] == "" {
|
||||||
|
return "", fmt.Errorf("reference selector components must not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(parts) {
|
||||||
|
case 1, 2:
|
||||||
|
return strings.Join(parts, "."), nil
|
||||||
|
case 3:
|
||||||
|
switch parts[1] {
|
||||||
|
case "extract", "merge", "normalize":
|
||||||
|
return strings.Join(parts, "."), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("three-component reference selector must use extract, merge, or normalize as its middle component")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("reference selector must use slot, chunk.slot, lane.slot, or lane.stage.slot")
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/notariusref/selector_test.go
Normal file
58
internal/notariusref/selector_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package notariusref
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeSelectorAcceptsDocumentedForms(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "pipeline", value: "party", want: "party"},
|
||||||
|
{name: "chunk", value: "chunk.party", want: "chunk.party"},
|
||||||
|
{name: "lane", value: "npc-registry.party", want: "npc-registry.party"},
|
||||||
|
{name: "extract", value: "npc-registry.extract.party", want: "npc-registry.extract.party"},
|
||||||
|
{name: "merge", value: "npc-registry.merge.party", want: "npc-registry.merge.party"},
|
||||||
|
{name: "normalize", value: "npc-registry.normalize.party", want: "npc-registry.normalize.party"},
|
||||||
|
{name: "whitespace", value: " npc-registry . extract . party ", want: "npc-registry.extract.party"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got, err := NormalizeSelector(test.value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NormalizeSelector(%q) error = %v", test.value, err)
|
||||||
|
}
|
||||||
|
if got != test.want {
|
||||||
|
t.Fatalf("NormalizeSelector(%q) = %q, want %q", test.value, got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSelectorRejectsInvalidForms(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "empty", value: "", wantErr: "required"},
|
||||||
|
{name: "whitespace", value: " ", wantErr: "required"},
|
||||||
|
{name: "empty first", value: ".party", wantErr: "components"},
|
||||||
|
{name: "empty middle", value: "lane..party", wantErr: "components"},
|
||||||
|
{name: "empty final", value: "lane.", wantErr: "components"},
|
||||||
|
{name: "equals", value: "party=/tmp/party.yml", wantErr: "must not contain"},
|
||||||
|
{name: "invalid stage", value: "lane.chunk.party", wantErr: "extract, merge, or normalize"},
|
||||||
|
{name: "too many components", value: "lane.extract.party.extra", wantErr: "must use"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, err := NormalizeSelector(test.value)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
||||||
|
t.Fatalf("NormalizeSelector(%q) error = %v, want containing %q", test.value, err, test.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -728,16 +728,8 @@ func resolvePreparedStableInput(sourceID string, paths artifacts.SessionPaths) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func preparedStableInputFilename(sourceID string) (string, bool) {
|
func preparedStableInputFilename(sourceID string) (string, bool) {
|
||||||
switch strings.TrimSpace(sourceID) {
|
descriptor, ok := artifactpolicy.DescribePreparedInputSource(sourceID)
|
||||||
case artifactpolicy.SourceInputPlayers:
|
return descriptor.Filename, ok
|
||||||
return "players.yml", true
|
|
||||||
case artifactpolicy.SourceInputParty:
|
|
||||||
return "party.yml", true
|
|
||||||
case artifactpolicy.SourceInputGlossary:
|
|
||||||
return "glossary.yml", true
|
|
||||||
default:
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAnalyzeRuntimeArtifactCatalog(
|
func buildAnalyzeRuntimeArtifactCatalog(
|
||||||
|
|||||||
Reference in New Issue
Block a user