Complete artifact provenance reporting
This commit is contained in:
@@ -164,7 +164,14 @@ and exact-integer regression coverage.
|
||||
|
||||
## 5. Complete provenance for generated artifacts and inferred dependencies
|
||||
|
||||
Status: Open — P3
|
||||
Status: Resolved — P3
|
||||
|
||||
Resolution: Generated catalog records now collect the complete base/profile family
|
||||
definition set from ConfigKit history. Effective source ownership for materialized
|
||||
dependency lists is rebuilt narrowly from retained explicit-list and input-edge
|
||||
origins, including mixed and generated-family dependencies. CLI tests verify a
|
||||
profiled family catalog agrees with generated source attribution and that inferred
|
||||
ordinary edges name their input contributor instead of the central default.
|
||||
|
||||
Two new reporting paths lose available declaration information:
|
||||
|
||||
|
||||
@@ -80,6 +80,35 @@ func TestConfigArtifactsReportsGeneratedFamilyProvenance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigArtifactsReportsProfiledFamilyContributors(t *testing.T) {
|
||||
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
|
||||
profilePath := filepath.Join(filepath.Dir(pipelinePath), "profiles", "testing.yml")
|
||||
mustWriteTestFile(t, profilePath, `scriptorium:
|
||||
artifact_families:
|
||||
character_note:
|
||||
prompt_id: dnd.character_note.testing
|
||||
`)
|
||||
|
||||
args := []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "testing"}
|
||||
var catalog bytes.Buffer
|
||||
if err := ConfigArtifacts(context.Background(), args, &catalog); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
records := catalogRecordsByKey(t, catalog.String())
|
||||
if got := strings.Split(records["character_note_arannis"][2], ","); !reflect.DeepEqual(got, []string{pipelinePath, profilePath}) {
|
||||
t.Fatalf("generated family definitions = %#v", got)
|
||||
}
|
||||
|
||||
var sources bytes.Buffer
|
||||
if err := ConfigSources(context.Background(), args, &sources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "scriptorium.artifacts.character_note_arannis.prompt_id\tfamily\t" + profilePath
|
||||
if !strings.Contains(sources.String(), want) {
|
||||
t.Fatalf("sources output missing profiled family contributor %q:\n%s", want, sources.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigArtifactsReportsBaseAndProfileContributors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||
|
||||
@@ -620,6 +620,66 @@ func TestConfigSourcesPreservesProfileOwnershipThroughFamilyExpansion(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSourcesAttributesInferredArtifactDependencies(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||
inputPath := filepath.Join(dir, "input-only.yml")
|
||||
profilePath := filepath.Join(dir, "profiles", "testing.yml")
|
||||
mustWriteTestFile(t, pipelinePath, `configkit:
|
||||
imports:
|
||||
- path: input-only.yml
|
||||
workspace:
|
||||
root: /srv/narratio
|
||||
whisperx:
|
||||
transcribe_url: https://transcription.example.test/transcribe
|
||||
scriptorium:
|
||||
artifacts:
|
||||
first:
|
||||
enabled: false
|
||||
output_path: artifacts/first.md
|
||||
second:
|
||||
enabled: false
|
||||
output_path: artifacts/second.md
|
||||
mixed:
|
||||
enabled: false
|
||||
output_path: artifacts/mixed.md
|
||||
depends_on: [first]
|
||||
`)
|
||||
mustWriteTestFile(t, inputPath, `scriptorium:
|
||||
artifacts:
|
||||
input_only:
|
||||
enabled: false
|
||||
output_path: artifacts/input-only.md
|
||||
inputs:
|
||||
prior:
|
||||
source: narratio.artifact.first
|
||||
`)
|
||||
mustWriteTestFile(t, profilePath, `scriptorium:
|
||||
artifacts:
|
||||
mixed:
|
||||
inputs:
|
||||
prior:
|
||||
source: narratio.artifact.second
|
||||
`)
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--profile", "testing"}, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"scriptorium.artifacts.input_only.depends_on\timport\t" + inputPath,
|
||||
"scriptorium.artifacts.mixed.depends_on\troot\t" + pipelinePath,
|
||||
"scriptorium.artifacts.mixed.depends_on\tprofile\t" + profilePath,
|
||||
} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Fatalf("sources output missing dependency contributor %q:\n%s", want, out.String())
|
||||
}
|
||||
}
|
||||
if strings.Contains(out.String(), "scriptorium.artifacts.input_only.depends_on\tdefault\tdefault") {
|
||||
t.Fatalf("inferred dependency retained default attribution:\n%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSourcesReportsRootImportProfileAndDefaultOwnership(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
rootPath := filepath.Join(dir, "pipeline.yml")
|
||||
|
||||
@@ -50,7 +50,7 @@ func EffectiveArtifactCatalog(cfg *PipelineConfig, party ResolvedParty) ([]Effec
|
||||
Dependencies: append([]string(nil), artifact.DependsOn...),
|
||||
}
|
||||
if member, ok := cfg.familyCatalog.Members[key]; ok {
|
||||
record.DefinitionSources = nonEmptySortedStrings([]string{member.Source})
|
||||
record.DefinitionSources = artifactFamilyDefinitionSources(cfg, member.Family)
|
||||
record.FamilyKey = member.Family
|
||||
record.CharacterID = member.CharacterID
|
||||
record.FamilySource = member.Source
|
||||
@@ -65,7 +65,21 @@ func EffectiveArtifactCatalog(cfg *PipelineConfig, party ResolvedParty) ([]Effec
|
||||
}
|
||||
|
||||
func artifactDefinitionSources(cfg *PipelineConfig, key string) []string {
|
||||
if cfg.resolution == nil {
|
||||
return declarationDefinitionSources(cfg,
|
||||
configkit.Keys("scriptorium", "artifacts", key),
|
||||
"scriptorium.artifacts."+key,
|
||||
)
|
||||
}
|
||||
|
||||
func artifactFamilyDefinitionSources(cfg *PipelineConfig, key string) []string {
|
||||
return declarationDefinitionSources(cfg,
|
||||
configkit.Keys("scriptorium", "artifact_families", key),
|
||||
"scriptorium.artifact_families."+key,
|
||||
)
|
||||
}
|
||||
|
||||
func declarationDefinitionSources(cfg *PipelineConfig, declaration configkit.Path, prefix string) []string {
|
||||
if cfg == nil || cfg.resolution == nil {
|
||||
return nil
|
||||
}
|
||||
pathsByID := make(map[string]string, len(cfg.resolution.configkitSources))
|
||||
@@ -78,7 +92,6 @@ func artifactDefinitionSources(cfg *PipelineConfig, key string) []string {
|
||||
}
|
||||
|
||||
seen := map[string]struct{}{}
|
||||
declaration := configkit.Keys("scriptorium", "artifacts", key)
|
||||
for _, change := range cfg.resolution.configkitMetadata.History(declaration) {
|
||||
if change.Origin.Layer != configkit.LayerBase && change.Origin.Layer != configkit.LayerProfile {
|
||||
continue
|
||||
@@ -92,7 +105,6 @@ func artifactDefinitionSources(cfg *PipelineConfig, key string) []string {
|
||||
}
|
||||
}
|
||||
if len(seen) == 0 {
|
||||
prefix := "scriptorium.artifacts." + key
|
||||
for _, ownership := range cfg.resolution.ownership {
|
||||
if ownership.path != prefix && !strings.HasPrefix(ownership.path, prefix+".") {
|
||||
continue
|
||||
|
||||
@@ -227,9 +227,39 @@ func effectivePipelineOwners(cfg *PipelineConfig) map[string][]string {
|
||||
for _, ownership := range cfg.resolution.ownership {
|
||||
owners[ownership.path] = append([]string(nil), ownership.sources...)
|
||||
}
|
||||
if cfg.Scriptorium != nil {
|
||||
for key, artifact := range cfg.Scriptorium.Artifacts {
|
||||
paths := artifactDependencyOwnershipPaths(key, artifact)
|
||||
if sources := sourcesForPipelinePaths(owners, paths); len(sources) != 0 {
|
||||
sort.Strings(sources)
|
||||
owners["scriptorium.artifacts."+key+".depends_on"] = sources
|
||||
}
|
||||
}
|
||||
}
|
||||
return owners
|
||||
}
|
||||
|
||||
func artifactDependencyOwnershipPaths(artifactKey string, artifact ScriptoriumArtifactConfig) []string {
|
||||
var paths []string
|
||||
for _, dependency := range artifact.DependsOn {
|
||||
for _, origin := range artifact.dependencyOrigins[dependency] {
|
||||
prefix := "scriptorium.artifacts." + artifactKey
|
||||
if origin.family != "" {
|
||||
prefix = "scriptorium.artifact_families." + origin.family
|
||||
}
|
||||
switch {
|
||||
case origin.inputName != "":
|
||||
paths = append(paths, prefix+".inputs."+origin.inputName+".source")
|
||||
case origin.family != "":
|
||||
paths = append(paths, prefix+".member_dependencies")
|
||||
default:
|
||||
paths = append(paths, prefix+".depends_on")
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func generatedArtifactFamilyForPath(path string, families ArtifactFamilyCatalog) (string, bool) {
|
||||
const prefix = "scriptorium.artifacts."
|
||||
if !strings.HasPrefix(path, prefix) {
|
||||
@@ -269,7 +299,7 @@ func generatedArtifactSources(path, familyKey string, families ArtifactFamilyCat
|
||||
case suffix == "output_path":
|
||||
candidates = []string{familyPrefix + "output_path_pattern"}
|
||||
case suffix == "depends_on":
|
||||
candidates = []string{familyPrefix + "depends_on", familyPrefix + "member_dependencies"}
|
||||
candidates = []string{path, familyPrefix + "depends_on", familyPrefix + "member_dependencies"}
|
||||
case strings.HasPrefix(suffix, "vars."):
|
||||
variable := strings.TrimPrefix(suffix, "vars.")
|
||||
candidates = []string{familyPrefix + "vars." + variable, familyPrefix + "member_vars." + variable}
|
||||
|
||||
Reference in New Issue
Block a user