Add helper to collect previous-session artifact input requirements
This commit is contained in:
112
internal/artifacts/previous_requirements.go
Normal file
112
internal/artifacts/previous_requirements.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package artifacts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PreviousArtifactRequirement describes one previous-session artifact dependency.
|
||||||
|
type PreviousArtifactRequirement struct {
|
||||||
|
Name string
|
||||||
|
Required bool
|
||||||
|
Sources []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectPreviousArtifactRequirements scans enabled Scriptorium artifacts and returns
|
||||||
|
// deduplicated previous-session artifact requirements in deterministic order.
|
||||||
|
func CollectPreviousArtifactRequirements(
|
||||||
|
artifactsCfg map[string]config.ScriptoriumArtifactConfig,
|
||||||
|
) []PreviousArtifactRequirement {
|
||||||
|
if len(artifactsCfg) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
artifactNames := sortedScriptoriumArtifactNames(artifactsCfg)
|
||||||
|
byName := map[string]PreviousArtifactRequirement{}
|
||||||
|
|
||||||
|
for _, artifactName := range artifactNames {
|
||||||
|
artifactCfg := artifactsCfg[artifactName]
|
||||||
|
if !artifactCfg.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inputNames := sortedScriptoriumInputKeys(artifactCfg.Inputs)
|
||||||
|
for _, inputName := range inputNames {
|
||||||
|
inputCfg := artifactCfg.Inputs[inputName]
|
||||||
|
previousName, ok := PreviousSessionArtifactName(inputCfg.Source)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
location := fmt.Sprintf(
|
||||||
|
"pipeline.scriptorium.artifacts.%s.inputs.%s.source",
|
||||||
|
artifactName,
|
||||||
|
inputName,
|
||||||
|
)
|
||||||
|
requirement := byName[previousName]
|
||||||
|
requirement.Name = previousName
|
||||||
|
requirement.Required = requirement.Required || inputCfg.Required
|
||||||
|
requirement.Sources = append(requirement.Sources, location)
|
||||||
|
byName[previousName] = requirement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(byName) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requirements := make([]PreviousArtifactRequirement, 0, len(byName))
|
||||||
|
for _, requirement := range byName {
|
||||||
|
requirement.Sources = dedupeAndSortStrings(requirement.Sources)
|
||||||
|
requirements = append(requirements, requirement)
|
||||||
|
}
|
||||||
|
sort.Slice(requirements, func(i, j int) bool {
|
||||||
|
return requirements[i].Name < requirements[j].Name
|
||||||
|
})
|
||||||
|
return requirements
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedScriptoriumArtifactNames(artifactsCfg map[string]config.ScriptoriumArtifactConfig) []string {
|
||||||
|
names := make([]string, 0, len(artifactsCfg))
|
||||||
|
for name := range artifactsCfg {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedScriptoriumInputKeys(inputs map[string]config.ScriptoriumInputConfig) []string {
|
||||||
|
if len(inputs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(inputs))
|
||||||
|
for name := range inputs {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeAndSortStrings(values []string) []string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
out := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
trimmed := strings.TrimSpace(value)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[trimmed]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[trimmed] = struct{}{}
|
||||||
|
out = append(out, trimmed)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
190
internal/artifacts/previous_requirements_test.go
Normal file
190
internal/artifacts/previous_requirements_test.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package artifacts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCollectPreviousArtifactRequirements(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
artifactsCfg map[string]config.ScriptoriumArtifactConfig
|
||||||
|
want []PreviousArtifactRequirement
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no artifacts",
|
||||||
|
artifactsCfg: nil,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no previous inputs",
|
||||||
|
artifactsCfg: map[string]config.ScriptoriumArtifactConfig{
|
||||||
|
"session_recap": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"transcript": {Source: "narratio.transcript.trimmed", Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one optional previous input",
|
||||||
|
artifactsCfg: map[string]config.ScriptoriumArtifactConfig{
|
||||||
|
"session_recap": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"previous_recap": {Source: "narratio.previous_session.artifact.session_recap", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PreviousArtifactRequirement{
|
||||||
|
{
|
||||||
|
Name: "session_recap",
|
||||||
|
Required: false,
|
||||||
|
Sources: []string{"pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one required previous input",
|
||||||
|
artifactsCfg: map[string]config.ScriptoriumArtifactConfig{
|
||||||
|
"quest_log": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"previous_quest_log": {Source: "narratio.previous_session.artifact.quest_log", Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PreviousArtifactRequirement{
|
||||||
|
{
|
||||||
|
Name: "quest_log",
|
||||||
|
Required: true,
|
||||||
|
Sources: []string{"pipeline.scriptorium.artifacts.quest_log.inputs.previous_quest_log.source"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate references are deduped",
|
||||||
|
artifactsCfg: map[string]config.ScriptoriumArtifactConfig{
|
||||||
|
"a": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"x": {Source: "narratio.previous_session.artifact.session_recap", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"y": {Source: "narratio.previous_session.artifact.session_recap", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PreviousArtifactRequirement{
|
||||||
|
{
|
||||||
|
Name: "session_recap",
|
||||||
|
Required: false,
|
||||||
|
Sources: []string{
|
||||||
|
"pipeline.scriptorium.artifacts.a.inputs.x.source",
|
||||||
|
"pipeline.scriptorium.artifacts.b.inputs.y.source",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "required plus optional reference becomes required",
|
||||||
|
artifactsCfg: map[string]config.ScriptoriumArtifactConfig{
|
||||||
|
"a": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"x": {Source: "narratio.previous_session.artifact.session_recap", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"y": {Source: "narratio.previous_session.artifact.session_recap", Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PreviousArtifactRequirement{
|
||||||
|
{
|
||||||
|
Name: "session_recap",
|
||||||
|
Required: true,
|
||||||
|
Sources: []string{
|
||||||
|
"pipeline.scriptorium.artifacts.a.inputs.x.source",
|
||||||
|
"pipeline.scriptorium.artifacts.b.inputs.y.source",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled artifact references are ignored",
|
||||||
|
artifactsCfg: map[string]config.ScriptoriumArtifactConfig{
|
||||||
|
"disabled_artifact": {
|
||||||
|
Enabled: false,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"x": {Source: "narratio.previous_session.artifact.session_recap", Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enabled_artifact": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"y": {Source: "narratio.previous_session.artifact.quest_log", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PreviousArtifactRequirement{
|
||||||
|
{
|
||||||
|
Name: "quest_log",
|
||||||
|
Required: false,
|
||||||
|
Sources: []string{"pipeline.scriptorium.artifacts.enabled_artifact.inputs.y.source"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deterministic ordering",
|
||||||
|
artifactsCfg: map[string]config.ScriptoriumArtifactConfig{
|
||||||
|
"zz": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"b_input": {Source: "narratio.previous_session.artifact.quest_log", Required: false},
|
||||||
|
"a_input": {Source: "narratio.previous_session.artifact.session_recap", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"aa": {
|
||||||
|
Enabled: true,
|
||||||
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||||
|
"c_input": {Source: "narratio.previous_session.artifact.session_recap", Required: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []PreviousArtifactRequirement{
|
||||||
|
{
|
||||||
|
Name: "quest_log",
|
||||||
|
Required: false,
|
||||||
|
Sources: []string{"pipeline.scriptorium.artifacts.zz.inputs.b_input.source"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "session_recap",
|
||||||
|
Required: false,
|
||||||
|
Sources: []string{
|
||||||
|
"pipeline.scriptorium.artifacts.aa.inputs.c_input.source",
|
||||||
|
"pipeline.scriptorium.artifacts.zz.inputs.a_input.source",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := CollectPreviousArtifactRequirements(tt.artifactsCfg)
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Fatalf("CollectPreviousArtifactRequirements() = %#v, want %#v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user