Add a framework mechanism for prepared modules and validators to contribute checkpoint identity fingerprints

This commit is contained in:
2026-07-20 15:14:41 -05:00
parent 87c57681f6
commit 0f25e7339f
15 changed files with 539 additions and 26 deletions

View File

@@ -16,6 +16,13 @@ type CheckpointFingerprint struct {
Value string `json:"value"`
}
// CheckpointFingerprintProvider supplies stable, non-secret semantic identity
// for a prepared module or validator. Values must not contain source content,
// credentials, local paths, timestamps, or other invocation-specific data.
type CheckpointFingerprintProvider interface {
CheckpointFingerprints() []CheckpointFingerprint
}
type CheckpointRecorder interface {
SourceRunning(moduleKey string) error
SourceSucceeded(moduleKey string, doc *source.SourceDocument) error

View File

@@ -72,6 +72,167 @@ func TestPrepareConstructsEverythingInStableOrder(t *testing.T) {
}
}
func TestPreparedCheckpointFingerprintsCollectEveryComponentInStableScopeOrder(t *testing.T) {
provider := func(value string) checkpointFingerprintTestProvider {
return checkpointFingerprintTestProvider{{Name: "identity", Value: value}}
}
prepared := &PreparedPipeline{
resolved: ResolvedPipeline{
ID: "fingerprints",
Input: Binding("input"),
Chunk: Binding("chunk"),
Output: Binding("output"),
},
input: provider("input"),
chunker: provider("chunk"),
chunkValidators: preparedValidatorChain{validators: []preparedValidator{{
resolved: ResolvedValidator{Binding: Binding("chunk-validator"), Target: ValidatorTargetChunk},
chunk: provider("chunk-validator"),
}}},
output: provider("output"),
}
lane := preparedLaneExecutor{
resolved: ResolvedArtifactLane{
ID: "spells",
Extract: Binding("extract"),
Merge: Binding("merge"),
Normalize: Binding("normalize"),
},
typed: &preparedTypedLane{
extractor: provider("extract"),
merger: provider("merge"),
normalizer: provider("normalize"),
},
extractValidators: fingerprintTestValidatorChain("extract-validator", provider("extract-validator")),
mergeValidators: fingerprintTestValidatorChain("merge-validator", provider("merge-validator")),
normalizeValidators: fingerprintTestValidatorChain("normalize-validator", provider("normalize-validator")),
}
prepared.lanes = []preparedLaneExecutor{lane}
first, err := collectPreparedCheckpointFingerprints(prepared)
if err != nil {
t.Fatalf("collectPreparedCheckpointFingerprints() error = %v, want nil", err)
}
second, err := collectPreparedCheckpointFingerprints(prepared)
if err != nil {
t.Fatalf("second collection error = %v, want nil", err)
}
if !reflect.DeepEqual(first, second) {
t.Fatalf("fingerprints are not deterministic: %#v / %#v", first, second)
}
if len(first) != 10 {
t.Fatalf("fingerprints = %#v, want all 10 prepared components", first)
}
for index := 1; index < len(first); index++ {
if first[index-1].Name >= first[index].Name {
t.Fatalf("fingerprints are not sorted: %#v", first)
}
}
prepared.checkpointFingerprints = first
copy := prepared.CheckpointFingerprints()
copy[0].Value = "mutated"
if reflect.DeepEqual(copy, prepared.CheckpointFingerprints()) {
t.Fatal("CheckpointFingerprints() exposed mutable backing storage")
}
}
func TestPreparedCheckpointFingerprintsValidateProviderValues(t *testing.T) {
tests := []struct {
name string
fingerprints []CheckpointFingerprint
want string
}{
{name: "empty name", fingerprints: []CheckpointFingerprint{{Value: "value"}}, want: "non-empty name and value"},
{name: "empty value", fingerprints: []CheckpointFingerprint{{Name: "name"}}, want: "non-empty name and value"},
{name: "duplicate after trimming", fingerprints: []CheckpointFingerprint{{Name: "same", Value: "one"}, {Name: " same ", Value: "two"}}, want: "duplicated"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
prepared := &PreparedPipeline{
resolved: ResolvedPipeline{ID: "fingerprints", Input: Binding("input"), Chunk: Binding("chunk"), Output: Binding("output")},
input: checkpointFingerprintTestProvider(test.fingerprints),
}
_, err := collectPreparedCheckpointFingerprints(prepared)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want substring %q", err, test.want)
}
})
}
}
func TestPreparedCheckpointFingerprintsRemainEmptyWithoutProviders(t *testing.T) {
registries, _ := constructionRegistries(t, nil, nil)
resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog())
if err != nil {
t.Fatal(err)
}
prepared, err := Prepare(resolved, registries, ModuleDependencies{})
if err != nil {
t.Fatal(err)
}
if got := prepared.CheckpointFingerprints(); len(got) != 0 {
t.Fatalf("CheckpointFingerprints() = %#v, want empty", got)
}
}
func TestPreparedCheckpointFingerprintsScopeRepeatedValidatorsByPosition(t *testing.T) {
provider := checkpointFingerprintTestProvider{{Name: "identity", Value: "same"}}
validator := func() preparedValidator {
return preparedValidator{resolved: ResolvedValidator{Binding: Binding("configured"), Target: ValidatorTargetChunk}, chunk: provider}
}
prepared := &PreparedPipeline{
resolved: ResolvedPipeline{ID: "fingerprints", Input: Binding("input"), Chunk: Binding("chunk"), Output: Binding("output")},
chunkValidators: preparedValidatorChain{validators: []preparedValidator{validator(), validator()}},
}
got, err := collectPreparedCheckpointFingerprints(prepared)
if err != nil {
t.Fatalf("collect repeated validators: %v", err)
}
if len(got) != 2 || got[0].Name == got[1].Name {
t.Fatalf("fingerprints = %#v, want independently scoped repeated validators", got)
}
}
type checkpointFingerprintTestProvider []CheckpointFingerprint
func (p checkpointFingerprintTestProvider) CheckpointFingerprints() []CheckpointFingerprint {
return append([]CheckpointFingerprint(nil), p...)
}
func (checkpointFingerprintTestProvider) Key() string { return "fingerprint-test" }
func (checkpointFingerprintTestProvider) ReferenceSlots() []contracts.ReferenceSlot { return nil }
func (checkpointFingerprintTestProvider) Parse(context.Context, contracts.ParseRequest) (*source.SourceDocument, error) {
return typedTestDocument(), nil
}
func (checkpointFingerprintTestProvider) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
return contracts.ChunkPlanResult{}, nil
}
func (checkpointFingerprintTestProvider) Encode(context.Context, contracts.OutputRequest) (contracts.OutputResult, error) {
return contracts.OutputResult{}, nil
}
func (checkpointFingerprintTestProvider) Name() string { return "fingerprint-test" }
func (checkpointFingerprintTestProvider) ExecutionClass() contracts.ExecutionClass {
return contracts.ExecutionClassDeterministic
}
func (checkpointFingerprintTestProvider) Validate(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
return contracts.ValidationResult{Approved: true}, nil
}
type checkpointFingerprintOutput struct {
contracts.OutputEncoder
fingerprints []CheckpointFingerprint
}
func (o checkpointFingerprintOutput) CheckpointFingerprints() []CheckpointFingerprint {
return append([]CheckpointFingerprint(nil), o.fingerprints...)
}
func fingerprintTestValidatorChain(key string, provider checkpointFingerprintTestProvider) preparedValidatorChain {
return preparedValidatorChain{validators: []preparedValidator{{
resolved: ResolvedValidator{Binding: Binding(key), Target: ValidatorTargetTyped},
typed: provider,
}}}
}
func TestPrepareDeliversTargetReferencesAsIndependentBuildInputs(t *testing.T) {
var built []string
var observations []constructionBuildObservation
@@ -183,9 +344,26 @@ func TestPrepareFailuresOccurBeforeInputParse(t *testing.T) {
}
}
func TestPrepareRejectsInvalidComponentCheckpointFingerprint(t *testing.T) {
failure := &constructionFailure{outputFingerprints: []CheckpointFingerprint{{Name: "identity"}}}
registries, input := constructionRegistries(t, nil, failure)
resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog())
if err != nil {
t.Fatal(err)
}
_, err = Prepare(resolved, registries, ModuleDependencies{})
if err == nil || !strings.Contains(err.Error(), "checkpoint fingerprint") || !strings.Contains(err.Error(), "output:output") {
t.Fatalf("Prepare() error = %v, want scoped checkpoint fingerprint error", err)
}
if len(input.requests) != 0 {
t.Fatalf("input Parse calls = %d, want zero", len(input.requests))
}
}
type constructionFailure struct {
requireExtractorLLM bool
output error
outputFingerprints []CheckpointFingerprint
}
func constructionProfile() PipelineProfile {
@@ -298,7 +476,11 @@ func constructionRegistriesWithHooks(t *testing.T, built *[]string, failure *con
if failure.output != nil {
return nil, failure.output
}
return &typedTestOutput{key: "output"}, nil
output := contracts.OutputEncoder(&typedTestOutput{key: "output"})
if failure.outputFingerprints != nil {
output = checkpointFingerprintOutput{OutputEncoder: output, fingerprints: failure.outputFingerprints}
}
return output, nil
}); err != nil {
t.Fatal(err)
}

View File

@@ -17,13 +17,14 @@ type PreparedPipeline struct {
ArtifactLanes []PreparedArtifactLane
Output ModuleBinding
resolved ResolvedPipeline
dependencies ModuleDependencies
input contracts.InputAdapter
chunker contracts.Chunker
chunkValidators preparedValidatorChain
lanes []preparedLaneExecutor
output contracts.OutputEncoder
resolved ResolvedPipeline
dependencies ModuleDependencies
input contracts.InputAdapter
chunker contracts.Chunker
chunkValidators preparedValidatorChain
lanes []preparedLaneExecutor
output contracts.OutputEncoder
checkpointFingerprints []CheckpointFingerprint
}
type PreparedArtifactLane struct {
@@ -114,6 +115,10 @@ func Prepare(resolved ResolvedPipeline, registries Registries, deps ModuleDepend
return nil, constructionError(stable.ID, "", StageOutput, stable.Output.Module, "", err)
}
prepared.output = output
prepared.checkpointFingerprints, err = collectPreparedCheckpointFingerprints(prepared)
if err != nil {
return nil, err
}
return prepared, nil
}

View File

@@ -0,0 +1,86 @@
package pipeline
import (
"fmt"
"sort"
"strings"
)
type checkpointFingerprintComponent struct {
scope string
module any
}
// CheckpointFingerprints returns a defensive copy of the stable semantic
// identities contributed by the pipeline's prepared modules and validators.
func (p *PreparedPipeline) CheckpointFingerprints() []CheckpointFingerprint {
if p == nil {
return nil
}
return append([]CheckpointFingerprint(nil), p.checkpointFingerprints...)
}
func collectPreparedCheckpointFingerprints(prepared *PreparedPipeline) ([]CheckpointFingerprint, error) {
components := []checkpointFingerprintComponent{
{scope: "input:" + prepared.resolved.Input.Module, module: prepared.input},
{scope: "chunk:" + prepared.resolved.Chunk.Module, module: prepared.chunker},
}
components = appendValidatorFingerprintComponents(components, "chunk:"+prepared.resolved.Chunk.Module, prepared.chunkValidators)
for _, lane := range prepared.lanes {
laneScope := func(stage ModuleStage, moduleKey string) string {
return string(stage) + ":" + lane.resolved.ID + ":" + moduleKey
}
components = append(components, checkpointFingerprintComponent{scope: laneScope(StageExtract, lane.resolved.Extract.Module), module: lane.typed.extractor})
components = appendValidatorFingerprintComponents(components, laneScope(StageExtract, lane.resolved.Extract.Module), lane.extractValidators)
components = append(components, checkpointFingerprintComponent{scope: laneScope(StageMerge, lane.resolved.Merge.Module), module: lane.typed.merger})
components = appendValidatorFingerprintComponents(components, laneScope(StageMerge, lane.resolved.Merge.Module), lane.mergeValidators)
components = append(components, checkpointFingerprintComponent{scope: laneScope(StageNormalize, lane.resolved.Normalize.Module), module: lane.typed.normalizer})
components = appendValidatorFingerprintComponents(components, laneScope(StageNormalize, lane.resolved.Normalize.Module), lane.normalizeValidators)
}
components = append(components, checkpointFingerprintComponent{scope: "output:" + prepared.resolved.Output.Module, module: prepared.output})
var values []CheckpointFingerprint
seen := make(map[string]struct{})
for _, item := range components {
provider, ok := item.module.(CheckpointFingerprintProvider)
if !ok {
continue
}
for _, fingerprint := range provider.CheckpointFingerprints() {
name := strings.TrimSpace(fingerprint.Name)
value := strings.TrimSpace(fingerprint.Value)
if name == "" || value == "" {
return nil, fmt.Errorf("pipeline %q checkpoint fingerprint for %s must have a non-empty name and value", prepared.resolved.ID, item.scope)
}
scopedName := item.scope + ":" + name
if _, exists := seen[scopedName]; exists {
return nil, fmt.Errorf("pipeline %q checkpoint fingerprint %q is duplicated", prepared.resolved.ID, scopedName)
}
seen[scopedName] = struct{}{}
values = append(values, CheckpointFingerprint{Name: scopedName, Value: value})
}
}
sort.Slice(values, func(i, j int) bool { return values[i].Name < values[j].Name })
return values, nil
}
func appendValidatorFingerprintComponents(components []checkpointFingerprintComponent, ownerScope string, chain preparedValidatorChain) []checkpointFingerprintComponent {
for index, validator := range chain.validators {
scope := fmt.Sprintf("%s:validator:%d:%s", ownerScope, index, validator.resolved.Binding.Module)
components = append(components, checkpointFingerprintComponent{scope: scope, module: preparedValidatorImplementation(validator)})
}
return components
}
func preparedValidatorImplementation(validator preparedValidator) any {
switch validator.resolved.Target {
case ValidatorTargetTyped:
return validator.typed
case ValidatorTargetChunk:
return validator.chunk
case ValidatorTargetSerialized:
return validator.serialized
default:
return nil
}
}