Remove retired lock authorities
This commit is contained in:
@@ -130,7 +130,6 @@ type PublishConfig struct {
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
UploadRun *bool `yaml:"upload_run"`
|
||||
Outputs []PublishOutputRule `yaml:"outputs"`
|
||||
Locks []PublishLockRule `yaml:"locks"`
|
||||
}
|
||||
|
||||
// PublishOutputRule configures one source-to-destination publish mapping.
|
||||
@@ -140,18 +139,6 @@ type PublishOutputRule struct {
|
||||
Required *bool `yaml:"required"`
|
||||
}
|
||||
|
||||
// PublishLockRule prevents one source from overwriting its top-level published
|
||||
// destination.
|
||||
type PublishLockRule struct {
|
||||
Source string `yaml:"source"`
|
||||
Reason string `yaml:"reason"`
|
||||
}
|
||||
|
||||
// PublishLockStore is the mutable per-session remote lock store.
|
||||
type PublishLockStore struct {
|
||||
Locks []PublishLockRule `yaml:"locks"`
|
||||
}
|
||||
|
||||
// WhisperXConfig configures WhisperX adapter settings.
|
||||
type WhisperXConfig struct {
|
||||
TranscribeURL string `yaml:"transcribe_url"`
|
||||
|
||||
@@ -10,7 +10,10 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const pipelineDefaultOwnershipSource = "default"
|
||||
const (
|
||||
pipelineDefaultOwnershipSource = "default"
|
||||
effectivePipelineDigestVersion = "narratio-effective-pipeline-v2\n"
|
||||
)
|
||||
|
||||
// PipelineProfileProvenance identifies the profile selected while resolving a
|
||||
// pipeline. It contains only non-secret selection metadata.
|
||||
@@ -98,7 +101,8 @@ func recomputePipelineEffectiveDigest(cfg *PipelineConfig) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
digest := sha256.Sum256(canonical)
|
||||
digestInput := append([]byte(effectivePipelineDigestVersion), canonical...)
|
||||
digest := sha256.Sum256(digestInput)
|
||||
cfg.resolution.effectiveDigest = hex.EncodeToString(digest[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -177,38 +177,6 @@ func LoadSessionBytesWithOptions(label string, data []byte, opts SessionLoadOpti
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// LoadPublishLockStoreBytes loads a mutable session lock store with strict
|
||||
// field checking and source validation.
|
||||
func LoadPublishLockStoreBytes(
|
||||
label string,
|
||||
data []byte,
|
||||
scriptorium *ScriptoriumConfig,
|
||||
notarius *NotariusConfig,
|
||||
) (*PublishLockStore, error) {
|
||||
var store PublishLockStore
|
||||
if err := decodeStrictYAMLFromReader("publish lock store", label, strings.NewReader(string(data)), &store); err != nil {
|
||||
return nil, fmt.Errorf("load publish lock store: %w", err)
|
||||
}
|
||||
locks, err := ValidatePublishLockRules(store.Locks, scriptorium, notarius, "locks")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load publish lock store: %w", err)
|
||||
}
|
||||
store.Locks = locks
|
||||
return &store, nil
|
||||
}
|
||||
|
||||
// MarshalPublishLockStore serializes a mutable lock store as strict-compatible YAML.
|
||||
func MarshalPublishLockStore(store *PublishLockStore) ([]byte, error) {
|
||||
if store == nil {
|
||||
store = &PublishLockStore{}
|
||||
}
|
||||
data, err := yaml.Marshal(store)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal publish lock store: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Load loads and resolves combined pipeline, campaign, and session configuration.
|
||||
// Passing only a session path is supported for package-internal compatibility;
|
||||
// in that form campaign.yml is expected next to the session file.
|
||||
|
||||
@@ -43,13 +43,6 @@ func TestLoadersRejectTrailingYAMLDocuments(t *testing.T) {
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "publish lock store",
|
||||
load: func() error {
|
||||
_, err := LoadPublishLockStoreBytes("s3://bucket/locks.yml", []byte("locks: []\n---\n{}\n"), nil, nil)
|
||||
return err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -571,13 +571,6 @@ func TestExtractionReferencesRequireDeclaredOutput(t *testing.T) {
|
||||
`,
|
||||
wantErr: []string{"pipeline.publish.outputs[0].source", "not defined"},
|
||||
},
|
||||
{
|
||||
name: "publish lock declared extraction",
|
||||
body: declared + `publish:
|
||||
locks:
|
||||
- source: narratio.extraction.npc_registry
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -492,124 +492,16 @@ func TestScriptoriumOutputPathsIgnoreInvalidKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishLockValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pipelineYML string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid built in source",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- source: narratio.transcript.final_trimmed
|
||||
reason: reviewed transcript
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "valid configured source",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
scriptorium:
|
||||
artifacts:
|
||||
session_recap:
|
||||
enabled: true
|
||||
prompt_id: dnd.session_recap
|
||||
output_path: artifacts/session_recap.md
|
||||
publish:
|
||||
locks:
|
||||
- source: narratio.artifact.session_recap
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "missing source rejected",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- reason: no source
|
||||
`,
|
||||
wantErr: "pipeline.publish.locks[0].source is required",
|
||||
},
|
||||
{
|
||||
name: "invalid source rejected",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- source: narratio.unknown
|
||||
`,
|
||||
wantErr: "pipeline.publish.locks[0].source \"narratio.unknown\" is unsupported",
|
||||
},
|
||||
{
|
||||
name: "prepared input source rejected",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- source: narratio.input.players
|
||||
`,
|
||||
wantErr: "pipeline.publish.locks[0].source \"narratio.input.players\" is unsupported",
|
||||
},
|
||||
{
|
||||
name: "duplicate source rejected",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- source: narratio.transcript.final_trimmed
|
||||
- source: " narratio.transcript.final_trimmed "
|
||||
`,
|
||||
wantErr: "duplicates another publish lock source",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, tt.pipelineYML, testSessionBaseYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("Validate() error = %v, want to contain %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishLockUnsupportedTranscriptSourceRejected(t *testing.T) {
|
||||
source := "narratio.transcript." + "merged"
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- source: ` + source + `
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "pipeline.publish.locks[0].source") || !strings.Contains(err.Error(), "unsupported") {
|
||||
t.Fatalf("Validate() error = %v, want source field and unsupported category", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishLockUnknownFieldFailsStrictDecode(t *testing.T) {
|
||||
func TestPublishLocksFieldFailsStrictDecode(t *testing.T) {
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||
_, err := Load(pipelinePath, sessionPath)
|
||||
if err == nil || !strings.Contains(err.Error(), "strict decode failed") {
|
||||
t.Fatalf("Load() error = %v, want strict decode failed", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "strict decode failed") || !strings.Contains(err.Error(), "field locks not found") {
|
||||
t.Fatalf("Load() error = %v, want retired locks field rejected", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,54 +519,6 @@ publish:
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishLockStoreBytesStrictDecodeAndValidation(t *testing.T) {
|
||||
store, err := LoadPublishLockStoreBytes("locks.yml", []byte(`locks:
|
||||
- source: narratio.transcript.final_trimmed
|
||||
reason: reviewed
|
||||
`), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPublishLockStoreBytes() error = %v", err)
|
||||
}
|
||||
if len(store.Locks) != 1 || store.Locks[0].Source != "narratio.transcript.final_trimmed" || store.Locks[0].Reason != "reviewed" {
|
||||
t.Fatalf("locks = %#v", store.Locks)
|
||||
}
|
||||
|
||||
_, err = LoadPublishLockStoreBytes("locks.yml", []byte(`locks:
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
`), nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "strict decode failed") {
|
||||
t.Fatalf("unknown field error = %v, want strict decode failed", err)
|
||||
}
|
||||
|
||||
_, err = LoadPublishLockStoreBytes("locks.yml", []byte(`locks:
|
||||
- source: narratio.transcript.final_trimmed
|
||||
- source: narratio.transcript.final_trimmed
|
||||
`), nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "duplicates another publish lock source") {
|
||||
t.Fatalf("duplicate error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePublishLockRulesStaticWins(t *testing.T) {
|
||||
merged := MergePublishLockRules(
|
||||
[]PublishLockRule{{Source: "narratio.transcript.final_trimmed", Reason: "static"}},
|
||||
[]PublishLockRule{
|
||||
{Source: "narratio.transcript.final_trimmed", Reason: "remote"},
|
||||
{Source: "narratio.transcript.final", Reason: "remote full"},
|
||||
},
|
||||
)
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("merged len = %d, want 2: %#v", len(merged), merged)
|
||||
}
|
||||
if merged[0].Source != "narratio.transcript.final_trimmed" || merged[0].Reason != "static" {
|
||||
t.Fatalf("merged[0] = %#v, want static lock", merged[0])
|
||||
}
|
||||
if merged[1].Source != "narratio.transcript.final" {
|
||||
t.Fatalf("merged[1] = %#v, want remote full lock", merged[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionAudioS3Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -83,71 +83,9 @@ func validatePublish(cfg *PublishConfig, scriptorium *ScriptoriumConfig, notariu
|
||||
}
|
||||
seenDest[normalizedDest] = struct{}{}
|
||||
}
|
||||
locks, err := ValidatePublishLockRules(cfg.Locks, scriptorium, notarius, "pipeline.publish.locks")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Locks = locks
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePublishLockRules validates and normalizes source-based publish locks.
|
||||
func ValidatePublishLockRules(locks []PublishLockRule, scriptorium *ScriptoriumConfig, notarius *NotariusConfig, label string) ([]PublishLockRule, error) {
|
||||
seenLocks := map[string]struct{}{}
|
||||
out := make([]PublishLockRule, 0, len(locks))
|
||||
configuredOutputs := scriptoriumOutputPathMap(scriptorium)
|
||||
extractionOutputs := notariusOutputKeySet(notarius)
|
||||
if strings.TrimSpace(label) == "" {
|
||||
label = "publish.locks"
|
||||
}
|
||||
for i, item := range locks {
|
||||
prefix := fmt.Sprintf("%s[%d]", label, i)
|
||||
source := strings.TrimSpace(item.Source)
|
||||
if source == "" {
|
||||
return nil, fmt.Errorf("%s.source is required", prefix)
|
||||
}
|
||||
if _, err := artifactpolicy.ValidatePublishSourceWithExtractions(source, configuredOutputs, extractionOutputs); err != nil {
|
||||
return nil, fmt.Errorf("%s.source %q is unsupported: %w", prefix, item.Source, err)
|
||||
}
|
||||
if _, ok := seenLocks[source]; ok {
|
||||
return nil, fmt.Errorf("%s.source %q duplicates another publish lock source", prefix, source)
|
||||
}
|
||||
seenLocks[source] = struct{}{}
|
||||
out = append(out, PublishLockRule{
|
||||
Source: source,
|
||||
Reason: strings.TrimSpace(item.Reason),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MergePublishLockRules returns the union of static and remote locks. Static
|
||||
// locks win when both sources contain the same lock.
|
||||
func MergePublishLockRules(staticLocks, remoteLocks []PublishLockRule) []PublishLockRule {
|
||||
out := make([]PublishLockRule, 0, len(staticLocks)+len(remoteLocks))
|
||||
seen := map[string]struct{}{}
|
||||
for _, item := range staticLocks {
|
||||
source := strings.TrimSpace(item.Source)
|
||||
if source == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, PublishLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)})
|
||||
seen[source] = struct{}{}
|
||||
}
|
||||
for _, item := range remoteLocks {
|
||||
source := strings.TrimSpace(item.Source)
|
||||
if source == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[source]; ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, PublishLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)})
|
||||
seen[source] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func scriptoriumOutputPathMap(scriptorium *ScriptoriumConfig) map[string]string {
|
||||
out := map[string]string{}
|
||||
if scriptorium == nil || len(scriptorium.Artifacts) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user