Centralize Scriptorium input source policy across config, analyze, and previous-cache

This commit is contained in:
2026-05-23 15:50:29 +00:00
parent 094b0d2532
commit 9fe44cd00d
6 changed files with 223 additions and 45 deletions

View File

@@ -1,6 +1,7 @@
package artifactpolicy
import (
"errors"
"fmt"
"regexp"
"strings"
@@ -19,6 +20,11 @@ const (
var configuredSourceRE = regexp.MustCompile(`^narratio\.artifact\.([a-z][a-z0-9_]*)$`)
var previousSourceRE = regexp.MustCompile(`^narratio\.previous_session\.artifact\.([a-z][a-z0-9_]*)$`)
var (
ErrUnsupportedScriptoriumInputSource = errors.New("unsupported scriptorium input source")
ErrInvalidPreviousSessionSource = errors.New("invalid previous-session source format")
)
type SourceKind string
const (
@@ -34,6 +40,28 @@ type Source struct {
ConfiguredKey string
}
// ScriptoriumInputSourceDescriptor describes one validated Scriptorium input source.
type ScriptoriumInputSourceDescriptor struct {
Source Source
PreviousSession *PreviousSessionSourceDescriptor
}
// PreviousSessionSourceDescriptor describes one canonical previous-session input source.
type PreviousSessionSourceDescriptor struct {
SourceID string
ConfiguredKey string
ConfiguredSourceID string
}
// UnknownConfiguredArtifactError reports a source that references an undefined configured artifact key.
type UnknownConfiguredArtifactError struct {
ConfiguredKey string
}
func (e *UnknownConfiguredArtifactError) Error() string {
return fmt.Sprintf("references unknown artifact %q", e.ConfiguredKey)
}
// ConfiguredSourceID converts a configured artifact key into source id form.
func ConfiguredSourceID(key string) string {
return configuredSourcePrefix + strings.TrimSpace(key)
@@ -83,6 +111,70 @@ func ClassifySource(source string) (Source, error) {
return Source{}, fmt.Errorf("unsupported artifact source %q", source)
}
// DescribeScriptoriumInputSource classifies one input source and returns descriptor
// metadata used by config validation, analyze input resolution, and previous-cache planning.
func DescribeScriptoriumInputSource(source string) (ScriptoriumInputSourceDescriptor, error) {
trimmed := strings.TrimSpace(source)
if trimmed == "" {
return ScriptoriumInputSourceDescriptor{}, ErrUnsupportedScriptoriumInputSource
}
if strings.HasPrefix(trimmed, "narratio.previous_session.artifact") {
descriptor, err := DescribePreviousSessionSource(trimmed)
if err != nil {
return ScriptoriumInputSourceDescriptor{}, err
}
return ScriptoriumInputSourceDescriptor{
Source: Source{
ID: descriptor.SourceID,
Kind: SourceKindPreviousArtifact,
ConfiguredKey: descriptor.ConfiguredKey,
},
PreviousSession: &descriptor,
}, nil
}
classified, err := ClassifySource(trimmed)
if err != nil {
return ScriptoriumInputSourceDescriptor{}, ErrUnsupportedScriptoriumInputSource
}
return ScriptoriumInputSourceDescriptor{Source: classified}, nil
}
// DescribePreviousSessionSource validates a canonical previous-session source id
// and returns both previous and configured-source vocabulary descriptors.
func DescribePreviousSessionSource(source string) (PreviousSessionSourceDescriptor, error) {
configuredKey, ok := ParsePreviousSessionSource(source)
if !ok {
return PreviousSessionSourceDescriptor{}, ErrInvalidPreviousSessionSource
}
return PreviousSessionSourceDescriptor{
SourceID: PreviousSessionSourceID(configuredKey),
ConfiguredKey: configuredKey,
ConfiguredSourceID: ConfiguredSourceID(configuredKey),
}, nil
}
// PreviousSessionSourceDescriptorForConfiguredKey derives a previous-session source descriptor
// from a configured artifact key.
func PreviousSessionSourceDescriptorForConfiguredKey(configuredKey string) (PreviousSessionSourceDescriptor, error) {
return DescribePreviousSessionSource(PreviousSessionSourceID(configuredKey))
}
// ValidateInputConfiguredReference checks that configured/previous-session sources
// reference configured artifacts known to the current Scriptorium config.
func ValidateInputConfiguredReference(
descriptor ScriptoriumInputSourceDescriptor,
configured map[string]struct{},
) error {
switch descriptor.Source.Kind {
case SourceKindConfiguredArtifact, SourceKindPreviousArtifact:
if _, ok := configured[descriptor.Source.ConfiguredKey]; !ok {
return &UnknownConfiguredArtifactError{ConfiguredKey: descriptor.Source.ConfiguredKey}
}
}
return nil
}
// ValidatePublishSource validates that a source is publish-compatible and references a known configured artifact.
func ValidatePublishSource(source string, configured map[string]string) (Source, error) {
classified, err := ClassifySource(source)

View File

@@ -1,6 +1,7 @@
package artifactpolicy
import (
"errors"
"strings"
"testing"
)
@@ -90,3 +91,101 @@ func TestResolvePublishedDestinationRejectsTraversal(t *testing.T) {
t.Fatal("ResolvePublishedDestination() error = nil, want traversal rejection")
}
}
func TestDescribeScriptoriumInputSource(t *testing.T) {
tests := []struct {
name string
source string
wantKind SourceKind
wantKey string
wantPrev bool
wantErr error
wantErrLike string
}{
{name: "built in", source: "narratio.transcript.final_trimmed", wantKind: SourceKindBuiltIn},
{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: "invalid previous", source: "narratio.previous_session.artifact.", wantErr: ErrInvalidPreviousSessionSource},
{name: "unsupported", source: "narratio.unknown", wantErr: ErrUnsupportedScriptoriumInputSource},
{name: "empty", source: " ", wantErr: ErrUnsupportedScriptoriumInputSource},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := DescribeScriptoriumInputSource(tt.source)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Fatalf("DescribeScriptoriumInputSource() error = %v, want %v", err, tt.wantErr)
}
return
}
if tt.wantErrLike != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErrLike) {
t.Fatalf("DescribeScriptoriumInputSource() error = %v, want like %q", err, tt.wantErrLike)
}
return
}
if err != nil {
t.Fatalf("DescribeScriptoriumInputSource() error = %v", err)
}
if got.Source.Kind != tt.wantKind {
t.Fatalf("DescribeScriptoriumInputSource().Source.Kind = %q, want %q", got.Source.Kind, tt.wantKind)
}
if got.Source.ConfiguredKey != tt.wantKey {
t.Fatalf("DescribeScriptoriumInputSource().Source.ConfiguredKey = %q, want %q", got.Source.ConfiguredKey, tt.wantKey)
}
if tt.wantPrev && got.PreviousSession == nil {
t.Fatal("DescribeScriptoriumInputSource().PreviousSession = nil, want descriptor")
}
if !tt.wantPrev && got.PreviousSession != nil {
t.Fatalf("DescribeScriptoriumInputSource().PreviousSession = %#v, want nil", got.PreviousSession)
}
})
}
}
func TestValidateInputConfiguredReference(t *testing.T) {
configured := map[string]struct{}{"session_recap": {}}
desc, err := DescribeScriptoriumInputSource("narratio.artifact.session_recap")
if err != nil {
t.Fatalf("DescribeScriptoriumInputSource(configured) error = %v", err)
}
if err := ValidateInputConfiguredReference(desc, configured); err != nil {
t.Fatalf("ValidateInputConfiguredReference(configured) error = %v", err)
}
prevDesc, err := DescribeScriptoriumInputSource("narratio.previous_session.artifact.session_recap")
if err != nil {
t.Fatalf("DescribeScriptoriumInputSource(previous) error = %v", err)
}
if err := ValidateInputConfiguredReference(prevDesc, configured); err != nil {
t.Fatalf("ValidateInputConfiguredReference(previous) error = %v", err)
}
missingDesc, err := DescribeScriptoriumInputSource("narratio.artifact.quest_log")
if err != nil {
t.Fatalf("DescribeScriptoriumInputSource(missing configured) error = %v", err)
}
err = ValidateInputConfiguredReference(missingDesc, configured)
var unknown *UnknownConfiguredArtifactError
if !errors.As(err, &unknown) || unknown.ConfiguredKey != "quest_log" {
t.Fatalf("ValidateInputConfiguredReference(missing configured) error = %v, want UnknownConfiguredArtifactError(quest_log)", err)
}
}
func TestPreviousSessionSourceDescriptorForConfiguredKey(t *testing.T) {
got, err := PreviousSessionSourceDescriptorForConfiguredKey("session_recap")
if err != nil {
t.Fatalf("PreviousSessionSourceDescriptorForConfiguredKey() error = %v", err)
}
if got.SourceID != "narratio.previous_session.artifact.session_recap" {
t.Fatalf("SourceID = %q, want narratio.previous_session.artifact.session_recap", got.SourceID)
}
if got.ConfiguredSourceID != "narratio.artifact.session_recap" {
t.Fatalf("ConfiguredSourceID = %q, want narratio.artifact.session_recap", got.ConfiguredSourceID)
}
if got.ConfiguredKey != "session_recap" {
t.Fatalf("ConfiguredKey = %q, want session_recap", got.ConfiguredKey)
}
}