Add versioned analysis fingerprint reconciliation

This commit is contained in:
2026-08-29 19:18:28 +00:00
parent ab5751459a
commit c32e0c401f
9 changed files with 1177 additions and 21 deletions

View File

@@ -451,7 +451,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
wantValidateErr: "pipeline.scriptorium.artifacts.session_recap.depends_on must not include itself",
},
{
name: "enabled dependency cycle fails validation",
name: "configured dependency cycle fails validation",
scriptoriumYAML: `scriptorium:
binary: scriptorium
artifacts:
@@ -476,7 +476,25 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
source: narratio.artifact.artifact_a
required: true
`,
wantValidateErr: "pipeline.scriptorium.artifacts enabled dependencies must not contain cycles",
wantValidateErr: "pipeline.scriptorium.artifacts dependencies must not contain cycles",
},
{
name: "disabled dependency cycle fails validation",
scriptoriumYAML: `scriptorium:
binary: scriptorium
artifacts:
artifact_a:
enabled: false
depends_on:
- artifact_b
output_path: artifacts/a.md
artifact_b:
enabled: false
depends_on:
- artifact_a
output_path: artifacts/b.md
`,
wantValidateErr: "pipeline.scriptorium.artifacts dependencies must not contain cycles",
},
{
name: "artifact source typo fails validation",

View File

@@ -761,7 +761,7 @@ func validateScriptorium(cfg *ScriptoriumConfig, notarius *NotariusConfig) error
}
}
if err := validateEnabledArtifactDependencyCycles(cfg.Artifacts); err != nil {
if err := ValidateScriptoriumArtifactDependencies(cfg.Artifacts); err != nil {
return err
}
@@ -983,38 +983,47 @@ func validatePathWithinRoot(fieldName, value, root string) error {
return fmt.Errorf("%s must be under %s/", fieldName, normalizedRoot)
}
func validateEnabledArtifactDependencyCycles(artifacts map[string]ScriptoriumArtifactConfig) error {
// ValidateScriptoriumArtifactDependencies validates the configured dependency
// graph independently of execution selection. Disabled artifacts remain valid
// prerequisites for explicit selections and therefore participate in cycles.
func ValidateScriptoriumArtifactDependencies(artifacts map[string]ScriptoriumArtifactConfig) error {
if len(artifacts) == 0 {
return nil
}
enabled := make(map[string]struct{}, len(artifacts))
graph := make(map[string][]string, len(artifacts))
for name, cfg := range artifacts {
if !cfg.Enabled {
continue
}
enabled[name] = struct{}{}
}
for name, cfg := range artifacts {
if !cfg.Enabled {
continue
if !artifactpolicy.IsConfiguredKey(name) {
return fmt.Errorf("pipeline.scriptorium.artifacts keys must match ^[a-z][a-z0-9_]*$")
}
seen := make(map[string]struct{}, len(cfg.DependsOn))
for _, dep := range cfg.DependsOn {
trimmedDep := strings.TrimSpace(dep)
if _, ok := enabled[trimmedDep]; ok {
graph[name] = append(graph[name], trimmedDep)
if trimmedDep == "" {
return fmt.Errorf("pipeline.scriptorium.artifacts.%s.depends_on entries must be non-empty", name)
}
if _, ok := artifacts[trimmedDep]; !ok {
return fmt.Errorf("pipeline.scriptorium.artifacts.%s dependency %q is not configured", name, dep)
}
if trimmedDep == name {
return fmt.Errorf("pipeline.scriptorium.artifacts.%s.depends_on must not include itself", name)
}
if _, duplicate := seen[trimmedDep]; duplicate {
continue
}
seen[trimmedDep] = struct{}{}
graph[name] = append(graph[name], trimmedDep)
}
sort.Strings(graph[name])
}
visiting := make(map[string]bool, len(enabled))
visited := make(map[string]bool, len(enabled))
visiting := make(map[string]bool, len(artifacts))
visited := make(map[string]bool, len(artifacts))
var visit func(node string) error
visit = func(node string) error {
if visiting[node] {
return fmt.Errorf("pipeline.scriptorium.artifacts enabled dependencies must not contain cycles")
return fmt.Errorf("pipeline.scriptorium.artifacts dependencies must not contain cycles")
}
if visited[node] {
return nil
@@ -1030,7 +1039,12 @@ func validateEnabledArtifactDependencyCycles(artifacts map[string]ScriptoriumArt
return nil
}
for node := range enabled {
nodes := make([]string, 0, len(artifacts))
for node := range artifacts {
nodes = append(nodes, node)
}
sort.Strings(nodes)
for _, node := range nodes {
if err := visit(node); err != nil {
return err
}