Centralize effective artifact selection
This commit is contained in:
@@ -138,10 +138,28 @@ func (c *ArtifactCatalog) RegisterBuiltIns() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterConfiguredArtifacts registers configured artifacts and applies executable selection.
|
||||
// RegisterConfiguredArtifacts registers configured artifacts. New runtime
|
||||
// callers should resolve selection first and use RegisterEffectiveConfiguredArtifacts.
|
||||
func (c *ArtifactCatalog) RegisterConfiguredArtifacts(
|
||||
configured map[string]ConfiguredArtifactDefinition,
|
||||
selected []string,
|
||||
selected ...[]string,
|
||||
) error {
|
||||
var requested []string
|
||||
if len(selected) > 0 {
|
||||
requested = selected[0]
|
||||
}
|
||||
effective, err := ResolveEffectiveArtifactSet(configured, requested)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.RegisterEffectiveConfiguredArtifacts(configured, effective)
|
||||
}
|
||||
|
||||
// RegisterEffectiveConfiguredArtifacts registers configured artifacts using one
|
||||
// resolved analyze execution set.
|
||||
func (c *ArtifactCatalog) RegisterEffectiveConfiguredArtifacts(
|
||||
configured map[string]ConfiguredArtifactDefinition,
|
||||
effective EffectiveArtifactSet,
|
||||
) error {
|
||||
keys := make([]string, 0, len(configured))
|
||||
for key := range configured {
|
||||
@@ -149,15 +167,6 @@ func (c *ArtifactCatalog) RegisterConfiguredArtifacts(
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
selectedSet := map[string]struct{}{}
|
||||
for _, key := range selected {
|
||||
trimmed := strings.TrimSpace(key)
|
||||
if !artifactpolicy.IsConfiguredKey(trimmed) {
|
||||
return fmt.Errorf("selected artifact key %q must match ^[a-z][a-z0-9_]*$", key)
|
||||
}
|
||||
selectedSet[trimmed] = struct{}{}
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
trimmed := strings.TrimSpace(key)
|
||||
if !artifactpolicy.IsConfiguredKey(trimmed) {
|
||||
@@ -169,11 +178,6 @@ func (c *ArtifactCatalog) RegisterConfiguredArtifacts(
|
||||
return fmt.Errorf("duplicate configured artifact key %q", trimmed)
|
||||
}
|
||||
|
||||
executable := def.Enabled
|
||||
if len(selectedSet) > 0 {
|
||||
_, executable = selectedSet[trimmed]
|
||||
}
|
||||
|
||||
if err := c.addEntry(CatalogEntry{
|
||||
SourceID: sourceID,
|
||||
ConfiguredKey: trimmed,
|
||||
@@ -181,24 +185,36 @@ func (c *ArtifactCatalog) RegisterConfiguredArtifacts(
|
||||
ProducerStage: "analyze",
|
||||
OutputKind: "scriptorium_artifact",
|
||||
Planned: true,
|
||||
Executable: executable,
|
||||
Executable: effective.Includes(trimmed),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register configured artifact %q: %w", trimmed, err)
|
||||
}
|
||||
c.configuredIndex[trimmed] = sourceID
|
||||
}
|
||||
|
||||
if len(selectedSet) > 0 {
|
||||
for key := range selectedSet {
|
||||
if _, ok := c.configuredIndex[key]; !ok {
|
||||
return fmt.Errorf("selected artifact %q is not configured", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BootstrapRuntimeCatalog creates the deterministic catalog definitions shared
|
||||
// by runtime consumers. Availability remains the caller's responsibility.
|
||||
func BootstrapRuntimeCatalog(
|
||||
configured map[string]ConfiguredArtifactDefinition,
|
||||
effective EffectiveArtifactSet,
|
||||
extraction map[string]ExtractionArtifactDefinition,
|
||||
) (*ArtifactCatalog, error) {
|
||||
catalog := NewArtifactCatalog()
|
||||
if err := catalog.RegisterBuiltIns(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := catalog.RegisterEffectiveConfiguredArtifacts(configured, effective); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := catalog.RegisterExtractionArtifacts(extraction); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return catalog, nil
|
||||
}
|
||||
|
||||
// Lookup returns one catalog entry by source ID.
|
||||
func (c *ArtifactCatalog) Lookup(sourceID string) (CatalogEntry, bool) {
|
||||
if c == nil {
|
||||
|
||||
98
internal/artifacts/effective_set.go
Normal file
98
internal/artifacts/effective_set.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
// EffectiveArtifactSet is the resolved set of configured artifacts that an
|
||||
// analyze invocation will execute.
|
||||
type EffectiveArtifactSet struct {
|
||||
keys []string
|
||||
resolved bool
|
||||
}
|
||||
|
||||
// ResolveEffectiveArtifactSet applies an explicit artifact selection when one
|
||||
// is supplied; otherwise it selects the configured enabled artifacts.
|
||||
func ResolveEffectiveArtifactSet(
|
||||
configured map[string]ConfiguredArtifactDefinition,
|
||||
selected []string,
|
||||
) (EffectiveArtifactSet, error) {
|
||||
configuredKeys := make(map[string]ConfiguredArtifactDefinition, len(configured))
|
||||
for key, definition := range configured {
|
||||
trimmed := strings.TrimSpace(key)
|
||||
if !artifactpolicy.IsConfiguredKey(trimmed) {
|
||||
return EffectiveArtifactSet{}, fmt.Errorf("configured artifact key %q must match ^[a-z][a-z0-9_]*$", key)
|
||||
}
|
||||
configuredKeys[trimmed] = definition
|
||||
}
|
||||
|
||||
if len(selected) > 0 {
|
||||
set := make(map[string]struct{}, len(selected))
|
||||
for _, key := range selected {
|
||||
trimmed := strings.TrimSpace(key)
|
||||
if !artifactpolicy.IsConfiguredKey(trimmed) {
|
||||
return EffectiveArtifactSet{}, fmt.Errorf("selected artifact key %q must match ^[a-z][a-z0-9_]*$", key)
|
||||
}
|
||||
if _, ok := configuredKeys[trimmed]; !ok {
|
||||
return EffectiveArtifactSet{}, fmt.Errorf("selected artifact %q is not configured", trimmed)
|
||||
}
|
||||
set[trimmed] = struct{}{}
|
||||
}
|
||||
return newEffectiveArtifactSet(set), nil
|
||||
}
|
||||
|
||||
set := make(map[string]struct{}, len(configuredKeys))
|
||||
for key, definition := range configuredKeys {
|
||||
if definition.Enabled {
|
||||
set[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
return newEffectiveArtifactSet(set), nil
|
||||
}
|
||||
|
||||
// ConfiguredArtifactDefinitions converts Scriptorium configuration into the
|
||||
// definition form used by the runtime catalog.
|
||||
func ConfiguredArtifactDefinitions(items map[string]config.ScriptoriumArtifactConfig) map[string]ConfiguredArtifactDefinition {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
definitions := make(map[string]ConfiguredArtifactDefinition, len(items))
|
||||
for key, item := range items {
|
||||
definitions[key] = ConfiguredArtifactDefinition{
|
||||
Enabled: item.Enabled,
|
||||
OutputPath: item.OutputPath,
|
||||
}
|
||||
}
|
||||
return definitions
|
||||
}
|
||||
|
||||
// Keys returns the selected configured artifact keys in deterministic order.
|
||||
func (s EffectiveArtifactSet) Keys() []string {
|
||||
return append([]string(nil), s.keys...)
|
||||
}
|
||||
|
||||
// Includes reports whether a configured artifact belongs to the effective set.
|
||||
func (s EffectiveArtifactSet) Includes(key string) bool {
|
||||
trimmed := strings.TrimSpace(key)
|
||||
index := sort.SearchStrings(s.keys, trimmed)
|
||||
return index < len(s.keys) && s.keys[index] == trimmed
|
||||
}
|
||||
|
||||
// Resolved reports whether the set was created by ResolveEffectiveArtifactSet.
|
||||
func (s EffectiveArtifactSet) Resolved() bool {
|
||||
return s.resolved
|
||||
}
|
||||
|
||||
func newEffectiveArtifactSet(set map[string]struct{}) EffectiveArtifactSet {
|
||||
keys := make([]string, 0, len(set))
|
||||
for key := range set {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return EffectiveArtifactSet{keys: keys, resolved: true}
|
||||
}
|
||||
86
internal/artifacts/effective_set_test.go
Normal file
86
internal/artifacts/effective_set_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
func TestResolveEffectiveArtifactSetUsesDefaultsOrExplicitSelection(t *testing.T) {
|
||||
configured := map[string]ConfiguredArtifactDefinition{
|
||||
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
|
||||
"player_handout": {Enabled: false, OutputPath: "artifacts/player_handout.md"},
|
||||
}
|
||||
|
||||
defaults, err := ResolveEffectiveArtifactSet(configured, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveEffectiveArtifactSet(defaults) error = %v", err)
|
||||
}
|
||||
if got, want := defaults.Keys(), []string{"session_recap"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("default keys = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
explicit, err := ResolveEffectiveArtifactSet(configured, []string{"player_handout"})
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveEffectiveArtifactSet(explicit) error = %v", err)
|
||||
}
|
||||
if got, want := explicit.Keys(), []string{"player_handout"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("explicit keys = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapRuntimeCatalogUsesEffectiveSetAndDeterministicDefinitions(t *testing.T) {
|
||||
configured := map[string]ConfiguredArtifactDefinition{
|
||||
"zeta": {Enabled: true, OutputPath: "artifacts/zeta.md"},
|
||||
"alpha": {Enabled: false, OutputPath: "artifacts/alpha.md"},
|
||||
}
|
||||
effective, err := ResolveEffectiveArtifactSet(configured, []string{"alpha"})
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveEffectiveArtifactSet() error = %v", err)
|
||||
}
|
||||
catalog, err := BootstrapRuntimeCatalog(configured, effective, map[string]ExtractionArtifactDefinition{
|
||||
"zeta_lane": {LaneID: "zeta"},
|
||||
"alpha_lane": {LaneID: "alpha"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BootstrapRuntimeCatalog() error = %v", err)
|
||||
}
|
||||
|
||||
configuredEntries := catalog.ListConfigured()
|
||||
if got, want := []string{configuredEntries[0].ConfiguredKey, configuredEntries[1].ConfiguredKey}, []string{"alpha", "zeta"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("configured order = %v, want %v", got, want)
|
||||
}
|
||||
if !configuredEntries[0].Executable || configuredEntries[1].Executable {
|
||||
t.Fatalf("configured executability = %+v, want only alpha executable", configuredEntries)
|
||||
}
|
||||
extractionEntries := catalog.ListExtraction()
|
||||
if got, want := []string{extractionEntries[0].ExtractionKey, extractionEntries[1].ExtractionKey}, []string{"alpha_lane", "zeta_lane"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("extraction order = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectPreviousArtifactRequirementsUsesExactEffectiveSet(t *testing.T) {
|
||||
configured := map[string]config.ScriptoriumArtifactConfig{
|
||||
"default_artifact": {
|
||||
Enabled: true,
|
||||
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||
"previous": {Source: "narratio.previous_session.artifact.default_artifact", Required: true},
|
||||
},
|
||||
},
|
||||
"explicit_artifact": {
|
||||
Enabled: false,
|
||||
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||
"previous": {Source: "narratio.previous_session.artifact.explicit_artifact", Required: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
effective, err := ResolveEffectiveArtifactSet(ConfiguredArtifactDefinitions(configured), []string{"explicit_artifact"})
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveEffectiveArtifactSet() error = %v", err)
|
||||
}
|
||||
requirements := CollectPreviousArtifactRequirements(configured, effective)
|
||||
if len(requirements) != 1 || requirements[0].Name != "explicit_artifact" || !requirements[0].Required {
|
||||
t.Fatalf("requirements = %#v, want only the explicit artifact prerequisite", requirements)
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,11 @@ type PreviousArtifactRequirement struct {
|
||||
Sources []string
|
||||
}
|
||||
|
||||
// CollectPreviousArtifactRequirements scans enabled Scriptorium artifacts and returns
|
||||
// deduplicated previous-session artifact requirements in deterministic order.
|
||||
// CollectPreviousArtifactRequirements scans the effective Scriptorium artifact
|
||||
// set and returns deduplicated previous-session requirements in deterministic order.
|
||||
func CollectPreviousArtifactRequirements(
|
||||
artifactsCfg map[string]config.ScriptoriumArtifactConfig,
|
||||
effective EffectiveArtifactSet,
|
||||
) []PreviousArtifactRequirement {
|
||||
if len(artifactsCfg) == 0 {
|
||||
return nil
|
||||
@@ -30,7 +31,7 @@ func CollectPreviousArtifactRequirements(
|
||||
|
||||
for _, artifactName := range artifactNames {
|
||||
artifactCfg := artifactsCfg[artifactName]
|
||||
if !artifactCfg.Enabled {
|
||||
if !effective.Includes(artifactName) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,11 @@ func TestCollectPreviousArtifactRequirements(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := CollectPreviousArtifactRequirements(tt.artifactsCfg)
|
||||
effective, err := ResolveEffectiveArtifactSet(ConfiguredArtifactDefinitions(tt.artifactsCfg), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveEffectiveArtifactSet() error = %v", err)
|
||||
}
|
||||
got := CollectPreviousArtifactRequirements(tt.artifactsCfg, effective)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("CollectPreviousArtifactRequirements() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user