Centralize effective artifact selection

This commit is contained in:
2026-08-10 22:27:06 +00:00
parent f39fc94610
commit af492c9e97
20 changed files with 457 additions and 130 deletions

View File

@@ -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 {

View 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}
}

View 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)
}
}

View File

@@ -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
}

View File

@@ -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)
}