Remove retired lock authorities

This commit is contained in:
2026-09-05 13:17:46 +00:00
parent 610eaefb27
commit 7e78c22fec
20 changed files with 73 additions and 723 deletions

View File

@@ -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"`

View File

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

View File

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

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

View File

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

View File

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

View File

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