Add Notarius configuration and extraction source policy

This commit is contained in:
2026-08-09 23:30:32 +00:00
parent 951383226c
commit 98734644d6
15 changed files with 678 additions and 30 deletions

View File

@@ -18,11 +18,14 @@ const (
SourceInputGlossary = "narratio.input.glossary"
configuredSourcePrefix = "narratio.artifact."
extractionSourcePrefix = "narratio.extraction."
previousConfiguredSrcPrefix = "narratio.previous_session.artifact."
)
var configuredSourceRE = regexp.MustCompile(`^narratio\.artifact\.([a-z][a-z0-9_]*)$`)
var extractionSourceRE = regexp.MustCompile(`^narratio\.extraction\.([a-z][a-z0-9_]*)$`)
var previousSourceRE = regexp.MustCompile(`^narratio\.previous_session\.artifact\.([a-z][a-z0-9_]*)$`)
var configuredKeyRE = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
var (
ErrUnsupportedScriptoriumInputSource = errors.New("unsupported scriptorium input source")
@@ -34,6 +37,7 @@ type SourceKind string
const (
SourceKindBuiltIn SourceKind = "built_in"
SourceKindConfiguredArtifact SourceKind = "configured_artifact"
SourceKindExtraction SourceKind = "extraction"
SourceKindPreviousArtifact SourceKind = "previous_session_configured_artifact"
SourceKindStableInput SourceKind = "stable_input"
)
@@ -67,11 +71,30 @@ func (e *UnknownConfiguredArtifactError) Error() string {
return fmt.Sprintf("references unknown artifact %q", e.ConfiguredKey)
}
// UnknownExtractionArtifactError reports a source that references an undefined extraction key.
type UnknownExtractionArtifactError struct {
ConfiguredKey string
}
func (e *UnknownExtractionArtifactError) Error() string {
return fmt.Sprintf("references unknown extraction output %q", e.ConfiguredKey)
}
// IsConfiguredKey reports whether a key follows the configured-artifact key grammar.
func IsConfiguredKey(key string) bool {
return configuredKeyRE.MatchString(strings.TrimSpace(key))
}
// ConfiguredSourceID converts a configured artifact key into source id form.
func ConfiguredSourceID(key string) string {
return configuredSourcePrefix + strings.TrimSpace(key)
}
// ExtractionSourceID converts an extraction output key into source id form.
func ExtractionSourceID(key string) string {
return extractionSourcePrefix + strings.TrimSpace(key)
}
// PreviousSessionSourceID converts a configured artifact key into previous-session source id form.
func PreviousSessionSourceID(key string) string {
return previousConfiguredSrcPrefix + strings.TrimSpace(key)
@@ -86,6 +109,15 @@ func ParseConfiguredSource(source string) (string, bool) {
return matches[1], true
}
// ParseExtractionSource extracts configured key from narratio.extraction.<key>.
func ParseExtractionSource(source string) (string, bool) {
matches := extractionSourceRE.FindStringSubmatch(strings.TrimSpace(source))
if len(matches) != 2 {
return "", false
}
return matches[1], true
}
// ParsePreviousSessionSource extracts configured key from narratio.previous_session.artifact.<key>.
func ParsePreviousSessionSource(source string) (string, bool) {
matches := previousSourceRE.FindStringSubmatch(strings.TrimSpace(source))
@@ -110,6 +142,9 @@ func ClassifySource(source string) (Source, error) {
if key, ok := ParseConfiguredSource(trimmed); ok {
return Source{ID: trimmed, Kind: SourceKindConfiguredArtifact, ConfiguredKey: key}, nil
}
if key, ok := ParseExtractionSource(trimmed); ok {
return Source{ID: trimmed, Kind: SourceKindExtraction, ConfiguredKey: key}, nil
}
if key, ok := ParsePreviousSessionSource(trimmed); ok {
return Source{ID: trimmed, Kind: SourceKindPreviousArtifact, ConfiguredKey: key}, nil
}
@@ -186,24 +221,48 @@ func PreviousSessionSourceDescriptorForConfiguredKey(configuredKey string) (Prev
func ValidateInputConfiguredReference(
descriptor ScriptoriumInputSourceDescriptor,
configured map[string]struct{},
) error {
return ValidateInputReference(descriptor, configured, nil)
}
// ValidateInputReference checks that configured and extraction sources are declared
// by the effective pipeline configuration.
func ValidateInputReference(
descriptor ScriptoriumInputSourceDescriptor,
configured map[string]struct{},
extractions map[string]struct{},
) error {
switch descriptor.Source.Kind {
case SourceKindConfiguredArtifact, SourceKindPreviousArtifact:
if _, ok := configured[descriptor.Source.ConfiguredKey]; !ok {
return &UnknownConfiguredArtifactError{ConfiguredKey: descriptor.Source.ConfiguredKey}
}
case SourceKindExtraction:
if _, ok := extractions[descriptor.Source.ConfiguredKey]; !ok {
return &UnknownExtractionArtifactError{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) {
return ValidatePublishSourceWithExtractions(source, configured, nil)
}
// ValidatePublishSourceWithExtractions validates publish sources against the
// configured Scriptorium artifacts and extraction outputs.
func ValidatePublishSourceWithExtractions(
source string,
configured map[string]string,
extractions map[string]struct{},
) (Source, error) {
classified, err := ClassifySource(source)
if err != nil {
return Source{}, fmt.Errorf("must be a built-in source id or narratio.artifact.<name>")
return Source{}, fmt.Errorf("must be a built-in source id, narratio.artifact.<name>, or configured narratio.extraction.<name>")
}
if classified.Kind == SourceKindPreviousArtifact {
return Source{}, fmt.Errorf("must be a built-in source id or narratio.artifact.<name>")
return Source{}, fmt.Errorf("must be a built-in source id, narratio.artifact.<name>, or configured narratio.extraction.<name>")
}
if classified.Kind == SourceKindConfiguredArtifact {
if configured == nil {
@@ -213,6 +272,11 @@ func ValidatePublishSource(source string, configured map[string]string) (Source,
return Source{}, fmt.Errorf("configured artifact %q is not defined in pipeline.scriptorium.artifacts", classified.ConfiguredKey)
}
}
if classified.Kind == SourceKindExtraction {
if _, ok := extractions[classified.ConfiguredKey]; !ok {
return Source{}, fmt.Errorf("extraction output %q is not defined in pipeline.notarius.outputs", classified.ConfiguredKey)
}
}
return classified, nil
}

View File

@@ -17,6 +17,7 @@ func TestClassifySource(t *testing.T) {
{name: "built in transcript", source: "narratio.transcript.final_trimmed", wantKind: SourceKindBuiltIn},
{name: "built in bounds", source: "narratio.bounds.session", wantKind: SourceKindBuiltIn},
{name: "configured artifact", source: "narratio.artifact.session_recap", wantKind: SourceKindConfiguredArtifact, wantKey: "session_recap"},
{name: "extraction", source: "narratio.extraction.npc_registry", wantKind: SourceKindExtraction, wantKey: "npc_registry"},
{name: "previous session configured", source: "narratio.previous_session.artifact.session_recap", wantKind: SourceKindPreviousArtifact, wantKey: "session_recap"},
{name: "unsupported", source: "narratio.unknown", wantErrLike: "unsupported artifact source"},
}
@@ -56,6 +57,60 @@ func TestValidatePublishSource(t *testing.T) {
}
}
func TestExtractionSourcePolicy(t *testing.T) {
if got := ExtractionSourceID(" npc_registry "); got != "narratio.extraction.npc_registry" {
t.Fatalf("ExtractionSourceID() = %q, want narratio.extraction.npc_registry", got)
}
if key, ok := ParseExtractionSource(" narratio.extraction.npc_registry "); !ok || key != "npc_registry" {
t.Fatalf("ParseExtractionSource() = %q, %t; want npc_registry, true", key, ok)
}
for _, source := range []string{
"narratio.extraction.",
"narratio.extraction.NPC",
"narratio.extraction.npc-registry",
"narratio.extraction.npc_registry.extra",
} {
if _, ok := ParseExtractionSource(source); ok {
t.Fatalf("ParseExtractionSource(%q) unexpectedly matched", source)
}
}
descriptor, err := DescribeScriptoriumInputSource("narratio.extraction.npc_registry")
if err != nil {
t.Fatalf("DescribeScriptoriumInputSource(extraction) error = %v", err)
}
if descriptor.Source.Kind != SourceKindExtraction || descriptor.Source.ConfiguredKey != "npc_registry" {
t.Fatalf("extraction descriptor = %#v", descriptor)
}
declared := map[string]struct{}{"npc_registry": {}}
if err := ValidateInputReference(descriptor, nil, declared); err != nil {
t.Fatalf("ValidateInputReference(declared extraction) error = %v", err)
}
if err := ValidateInputReference(descriptor, nil, nil); err == nil {
t.Fatal("ValidateInputReference(unknown extraction) error = nil, want error")
}
if _, err := ValidatePublishSourceWithExtractions("narratio.extraction.npc_registry", nil, declared); err != nil {
t.Fatalf("ValidatePublishSourceWithExtractions(declared) error = %v", err)
}
if _, err := ValidatePublishSourceWithExtractions("narratio.extraction.unknown", nil, declared); err == nil {
t.Fatal("ValidatePublishSourceWithExtractions(unknown) error = nil, want error")
}
identities := map[string]struct{}{}
for _, sourceID := range []string{
ExtractionSourceID("npc_registry"),
ConfiguredSourceID("npc_registry"),
PreviousSessionSourceID("npc_registry"),
SourceBoundsSession,
} {
if _, exists := identities[sourceID]; exists {
t.Fatalf("source identity collision at %q", sourceID)
}
identities[sourceID] = struct{}{}
}
}
func TestResolvePublishedDestination(t *testing.T) {
configured := map[string]string{"session_recap": "artifacts/session_recap.md"}