Add family artifact selection and provenance
This commit is contained in:
@@ -60,10 +60,14 @@ func resolveEffectiveArtifacts(cfg *config.Config, selected []string) (artifacts
|
||||
return artifacts.EffectiveArtifactSet{}, fmt.Errorf("--artifacts requires pipeline.scriptorium.artifacts to be configured")
|
||||
}
|
||||
configured := artifacts.ConfiguredArtifactDefinitions(cfg.Pipeline.Scriptorium.Artifacts)
|
||||
if len(selected) > 0 && len(configured) == 0 {
|
||||
normalized, err := normalizeArtifactSelection(cfg, selected)
|
||||
if err != nil {
|
||||
return artifacts.EffectiveArtifactSet{}, err
|
||||
}
|
||||
if len(normalized) > 0 && len(configured) == 0 {
|
||||
return artifacts.EffectiveArtifactSet{}, fmt.Errorf("--artifacts requires at least one configured artifact in pipeline.scriptorium.artifacts")
|
||||
}
|
||||
effective, err := artifacts.ResolveEffectiveArtifactSet(configured, selected)
|
||||
effective, err := artifacts.ResolveEffectiveArtifactSet(configured, normalized)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "is not configured") {
|
||||
return artifacts.EffectiveArtifactSet{}, fmt.Errorf("--artifacts includes unknown artifact %q", selectedArtifactName(err))
|
||||
@@ -73,7 +77,47 @@ func resolveEffectiveArtifacts(cfg *config.Config, selected []string) (artifacts
|
||||
if err := validateEffectiveArtifactConfiguration(cfg.Pipeline.Scriptorium.Artifacts, effective); err != nil {
|
||||
return artifacts.EffectiveArtifactSet{}, err
|
||||
}
|
||||
return effective, nil
|
||||
return effective.WithOrigins(effectiveArtifactOrigins(cfg.Pipeline)), nil
|
||||
}
|
||||
|
||||
func normalizeArtifactSelection(cfg *config.Config, selected []string) ([]string, error) {
|
||||
if len(selected) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
configured := cfg.Pipeline.Scriptorium.Artifacts
|
||||
families := config.ArtifactFamilies(cfg.Pipeline).Families
|
||||
set := make(map[string]struct{}, len(selected))
|
||||
for _, raw := range selected {
|
||||
key := strings.TrimSpace(raw)
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("artifact names must be non-empty")
|
||||
}
|
||||
if family, ok := families[key]; ok {
|
||||
for _, member := range family.Members {
|
||||
set[member] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := configured[key]; !ok {
|
||||
return nil, fmt.Errorf("--artifacts includes unknown artifact %q", key)
|
||||
}
|
||||
set[key] = struct{}{}
|
||||
}
|
||||
normalized := make([]string, 0, len(set))
|
||||
for key := range set {
|
||||
normalized = append(normalized, key)
|
||||
}
|
||||
sort.Strings(normalized)
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func effectiveArtifactOrigins(pipeline *config.PipelineConfig) map[string]artifacts.EffectiveArtifactOrigin {
|
||||
catalog := config.ArtifactFamilies(pipeline)
|
||||
origins := make(map[string]artifacts.EffectiveArtifactOrigin, len(catalog.Members))
|
||||
for key, member := range catalog.Members {
|
||||
origins[key] = artifacts.EffectiveArtifactOrigin{Family: member.Family, CharacterID: member.CharacterID}
|
||||
}
|
||||
return origins
|
||||
}
|
||||
|
||||
func selectedArtifactName(err error) string {
|
||||
|
||||
@@ -1,11 +1,71 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
func TestResolveEffectiveArtifactsExpandsFamilySelections(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(name, body string) string {
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
pipeline := write("pipeline.yml", `workspace: {root: /tmp/narratio-work}
|
||||
whisperx: {transcribe_url: https://example.test/transcribe}
|
||||
notification: {mode: noop}
|
||||
scriptorium:
|
||||
artifact_families:
|
||||
character_meta:
|
||||
enabled: false
|
||||
for_each: party.characters
|
||||
prompt_id: dnd.character_meta
|
||||
output_path_pattern: artifacts/characters/{character_id}/meta.md
|
||||
`)
|
||||
campaign := write("campaign.yml", `campaign_id: campaign
|
||||
inputs: {speakers_file: speakers.yml, autocorrect_file: autocorrect.yml, glossary_file: glossary.yml, party_file: party.yml}
|
||||
`)
|
||||
session := write("session.yml", `session_id: session
|
||||
campaign: campaign
|
||||
inputs: {audio_dir: audio}
|
||||
`)
|
||||
write("party.yml", `schema_version: narratio.party.v1
|
||||
characters:
|
||||
zeta: {player: {name: Z}, character: {name: Zeta, classes: [{name: wizard}]}}
|
||||
alpha: {player: {name: A}, character: {name: Alpha, classes: [{name: ranger}]}}
|
||||
`)
|
||||
cfg, err := config.LoadWithSessionOptions(pipeline, campaign, session, config.SessionLoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
effective, err := resolveEffectiveArtifacts(cfg, []string{"character_meta", "character_meta_alpha"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := effective.Keys(), []string{"character_meta_alpha", "character_meta_zeta"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("keys = %#v, want %#v", got, want)
|
||||
}
|
||||
if origin, ok := effective.Origin("character_meta_alpha"); !ok || origin.Family != "character_meta" || origin.CharacterID != "alpha" {
|
||||
t.Fatalf("origin = %#v, %t", origin, ok)
|
||||
}
|
||||
if _, err := resolveEffectiveArtifacts(cfg, []string{"unknown"}); err == nil || !strings.Contains(err.Error(), "unknown artifact") {
|
||||
t.Fatalf("unknown selection error = %v", err)
|
||||
}
|
||||
if defaultEffective, err := resolveEffectiveArtifacts(cfg, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(defaultEffective.Keys()) != 0 {
|
||||
t.Fatal("default selection should omit disabled family members")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactSelectionFlagNormalize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -43,12 +43,13 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
|
||||
}
|
||||
writeArtifactLine(out, artifacts.ArtifactBoundsSession, lockSet)
|
||||
fmt.Fprintln(out, "Configured:")
|
||||
origins := config.ArtifactFamilies(cfg.Pipeline).Members
|
||||
for _, entry := range catalog.ListConfigured() {
|
||||
state := "unavailable"
|
||||
if entry.Available {
|
||||
state = "available"
|
||||
}
|
||||
writeExtractionArtifactLine(out, entry.SourceID, state, entry.Provenance, lockSet)
|
||||
writeConfiguredArtifactLine(out, entry.SourceID, state, entry.Provenance, lockSet, origins)
|
||||
}
|
||||
fmt.Fprintln(out, "Extraction:")
|
||||
for _, entry := range catalog.ListExtraction() {
|
||||
@@ -70,6 +71,22 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfiguredArtifactLine(out io.Writer, source, state, provenance string, lockSet map[string]config.PublishLockRule, origins map[string]config.ArtifactFamilyMemberOrigin) {
|
||||
parts := []string{source, "planned", state}
|
||||
if key, ok := artifactpolicy.ParseConfiguredSource(source); ok {
|
||||
if origin, family := origins[key]; family {
|
||||
parts = append(parts, "family="+origin.Family, "character_id="+origin.CharacterID)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(provenance) != "" {
|
||||
parts = append(parts, "provenance="+strings.TrimSpace(provenance))
|
||||
}
|
||||
if _, ok := lockSet[source]; ok {
|
||||
parts = append(parts, "locked")
|
||||
}
|
||||
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
|
||||
}
|
||||
|
||||
func writeExtractionArtifactLine(out io.Writer, source, state, provenance string, lockSet map[string]config.PublishLockRule) {
|
||||
parts := []string{source, "planned", state}
|
||||
if strings.TrimSpace(provenance) != "" {
|
||||
|
||||
@@ -35,7 +35,11 @@ func Plan(ctx context.Context, args []string, out io.Writer) error {
|
||||
if err := config.Validate(cfg); err != nil {
|
||||
return fmt.Errorf("plan: %w", err)
|
||||
}
|
||||
effective, err := resolveEffectiveArtifacts(cfg, request.SelectedArtifacts)
|
||||
selectedArtifacts, err := normalizeArtifactSelection(cfg, request.SelectedArtifacts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("plan: %w", err)
|
||||
}
|
||||
effective, err := resolveEffectiveArtifacts(cfg, selectedArtifacts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("plan: %w", err)
|
||||
}
|
||||
@@ -55,7 +59,7 @@ func Plan(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
stages := request.Plan.Stages()
|
||||
stageEnv := &stage.Env{
|
||||
Config: cfg, SelectedArtifactKeys: append([]string(nil), request.SelectedArtifacts...),
|
||||
Config: cfg, SelectedArtifactKeys: append([]string(nil), selectedArtifacts...),
|
||||
EffectiveArtifacts: effective, ArtifactStore: store, Force: request.Force,
|
||||
}
|
||||
|
||||
@@ -226,7 +230,11 @@ func planArtifactList(values []stage.AnalyzeResumeArtifact) string {
|
||||
if value.Forced {
|
||||
detail += ":forced"
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s(%s)", value.Key, detail))
|
||||
identity := value.Key
|
||||
if value.Family != "" {
|
||||
identity += fmt.Sprintf("[family=%s character_id=%s]", value.Family, value.CharacterID)
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s(%s)", identity, detail))
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
@@ -29,13 +29,17 @@ func Run(ctx context.Context, args []string, out io.Writer) error {
|
||||
if err := config.Validate(cfg); err != nil {
|
||||
return fmt.Errorf("run: %w", err)
|
||||
}
|
||||
effectiveArtifacts, err := resolveEffectiveArtifacts(cfg, request.SelectedArtifacts)
|
||||
selectedArtifacts, err := normalizeArtifactSelection(cfg, request.SelectedArtifacts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run: %w", err)
|
||||
}
|
||||
effectiveArtifacts, err := resolveEffectiveArtifacts(cfg, selectedArtifacts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run: %w", err)
|
||||
}
|
||||
summary, err := executeStagesFn(ctx, cfg, request.Plan, RunOptions{
|
||||
Force: request.Force,
|
||||
SelectedArtifacts: request.SelectedArtifacts,
|
||||
SelectedArtifacts: selectedArtifacts,
|
||||
EffectiveArtifacts: effectiveArtifacts,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -216,13 +216,17 @@ func runSingleStageCommand(ctx context.Context, req singleStageCommand) (*RunSum
|
||||
if err := config.Validate(cfg); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
|
||||
}
|
||||
effectiveArtifacts, err := resolveEffectiveArtifacts(cfg, req.SelectedArtifacts)
|
||||
selectedArtifacts, err := normalizeArtifactSelection(cfg, req.SelectedArtifacts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
|
||||
}
|
||||
effectiveArtifacts, err := resolveEffectiveArtifacts(cfg, selectedArtifacts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
|
||||
}
|
||||
summary, err := executeStagesFn(ctx, cfg, plan, RunOptions{
|
||||
Force: req.Force,
|
||||
SelectedArtifacts: req.SelectedArtifacts,
|
||||
SelectedArtifacts: selectedArtifacts,
|
||||
EffectiveArtifacts: effectiveArtifacts,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user