Centralize builder request cloning
This commit is contained in:
@@ -51,11 +51,12 @@ until its producing step completes.
|
||||
|
||||
Preparation is the construction boundary. It validates the resolved shape and
|
||||
registry set, clones the resolved data, then constructs the input adapter,
|
||||
chunker, stage-local validators, every typed lane, and output encoder with
|
||||
cloned options, references, and shared dependencies. It also collects stable
|
||||
checkpoint fingerprints. Missing registrations, incompatible typed entries,
|
||||
nil implementations, and constructor failures are reported before source
|
||||
parsing or any stage operation begins.
|
||||
chunker, stage-local validators, every typed lane, and output encoder. Each
|
||||
registered builder receives its own cloned build request immediately before its
|
||||
module-owned code runs. Preparation also collects stable checkpoint
|
||||
fingerprints. Missing registrations, incompatible typed entries, nil
|
||||
implementations, and constructor failures are reported before source parsing
|
||||
or any stage operation begins.
|
||||
|
||||
An output encoder can opt into source-evidence publication through its output
|
||||
policy. Preparation keeps the configured lane allowlist and active lanes
|
||||
|
||||
@@ -305,6 +305,7 @@ func TestPrepareDeliversTargetReferencesAsIndependentBuildInputs(t *testing.T) {
|
||||
if name != "extract" {
|
||||
return
|
||||
}
|
||||
request.Options["nested"].(map[string]any)["value"] = "mutated by extractor builder"
|
||||
slot := request.References.Slots["extract"]
|
||||
slot.Items[0].Content = []byte("mutated by extractor builder")
|
||||
request.References.Slots["extract"] = slot
|
||||
@@ -318,6 +319,7 @@ func TestPrepareDeliversTargetReferencesAsIndependentBuildInputs(t *testing.T) {
|
||||
resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet = constructionReferenceSet("extract", "extract reference")
|
||||
resolved.Steps[0].ArtifactLanes[0].MergeReferences.ReferenceSet = constructionReferenceSet("merge", "merge reference")
|
||||
resolved.Steps[0].ArtifactLanes[0].NormalizeReferences.ReferenceSet = constructionReferenceSet("normalize", "normalize reference")
|
||||
resolved.Steps[0].ArtifactLanes[0].Extract.Options = constructionBuildRequest().Options
|
||||
|
||||
prepared, err := Prepare(resolved, registries, ModuleDependencies{})
|
||||
if err != nil {
|
||||
@@ -339,6 +341,15 @@ func TestPrepareDeliversTargetReferencesAsIndependentBuildInputs(t *testing.T) {
|
||||
if got := constructionReferenceContent(resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet); got != "extract reference" {
|
||||
t.Fatalf("resolved extract references = %q, want original content", got)
|
||||
}
|
||||
if got := constructionReferenceContent(prepared.resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet); got != "extract reference" {
|
||||
t.Fatalf("prepared extract references = %q, want original content", got)
|
||||
}
|
||||
if got := resolved.Steps[0].ArtifactLanes[0].Extract.Options["nested"].(map[string]any)["value"]; got != "original" {
|
||||
t.Fatalf("resolved extract options = %#v, want original nested value", got)
|
||||
}
|
||||
if got := prepared.resolved.Steps[0].ArtifactLanes[0].Extract.Options["nested"].(map[string]any)["value"]; got != "original" {
|
||||
t.Fatalf("prepared extract options = %#v, want original nested value", got)
|
||||
}
|
||||
|
||||
_, err = prepared.Steps[0].lanes[0].typed.extract(context.Background(), prepared.Steps[0].lanes[0].typed.extractor, contracts.TypedExtractionRequest{
|
||||
References: CloneReferenceSet(resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet),
|
||||
@@ -354,6 +365,163 @@ func TestPrepareDeliversTargetReferencesAsIndependentBuildInputs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredBuildersReceiveIndependentBuildRequests(t *testing.T) {
|
||||
request := constructionBuildRequest()
|
||||
probe := buildRequestMutationProbe{t: t, want: cloneBuildRequest(request)}
|
||||
|
||||
inputs := NewInputAdapterRegistry()
|
||||
if err := inputs.RegisterBuilderWithSpec(testModuleSpec("input", StageInput), rejectUnconfiguredOptions, func(request BuildRequest) (contracts.InputAdapter, error) {
|
||||
probe.observe(request)
|
||||
return &constructionInput{key: "input"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
chunkers := NewChunkerRegistry()
|
||||
if err := chunkers.RegisterBuilderWithSpec(testModuleSpec("chunk", StageChunk), rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Chunker, error) {
|
||||
probe.observe(request)
|
||||
return &typedTestChunker{key: "chunk"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outputs := NewOutputEncoderRegistry()
|
||||
if err := outputs.RegisterBuilderWithSpec(testModuleSpec("output", StageOutput), rejectUnconfiguredOptions, func(request BuildRequest) (contracts.OutputEncoder, error) {
|
||||
probe.observe(request)
|
||||
return &typedTestOutput{key: "output"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extractors := NewExtractorRegistry()
|
||||
extractSpec := testModuleSpec("extract", StageExtract)
|
||||
extractSpec.ArtifactKind = "test/notes"
|
||||
if err := RegisterExtractorBuilder(extractors, extractSpec, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Extractor[codecNotes], error) {
|
||||
probe.observe(request)
|
||||
return typedTestExtractor[codecNotes]{key: "extract"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mergers := NewMergerRegistry()
|
||||
mergeSpec := testModuleSpec("merge", StageMerge)
|
||||
mergeSpec.ArtifactKind = "test/notes"
|
||||
if err := RegisterMergerBuilder(mergers, mergeSpec, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Merger[codecNotes], error) {
|
||||
probe.observe(request)
|
||||
return typedTestMerger[codecNotes]{key: "merge"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
normalizers := NewNormalizerRegistry()
|
||||
normalizeSpec := testModuleSpec("normalize", StageNormalize)
|
||||
normalizeSpec.ArtifactKind = "test/notes"
|
||||
if err := RegisterNormalizerBuilder(normalizers, normalizeSpec, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Normalizer[codecNotes], error) {
|
||||
probe.observe(request)
|
||||
return typedTestNormalizer[codecNotes]{key: "normalize"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validators := NewValidatorRegistry()
|
||||
if err := RegisterChunkValidatorBuilder(validators, ValidatorSpec{Key: "chunk-validator", ExecutionClass: contracts.ExecutionClassDeterministic}, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.ChunkValidator, error) {
|
||||
probe.observe(request)
|
||||
return typedTestChunkValidator{key: "chunk-validator"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := RegisterTypedValidatorBuilder(validators, "test/notes", ValidatorSpec{Key: "typed-validator", ExecutionClass: contracts.ExecutionClassDeterministic}, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.TypedValidator[codecNotes], error) {
|
||||
probe.observe(request)
|
||||
return typedTestValidator[codecNotes]{key: "typed-validator"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := RegisterSerializedValidatorBuilder(validators, SerializedValidatorSpec{ValidatorSpec: ValidatorSpec{Key: "serialized-validator", ExecutionClass: contracts.ExecutionClassDeterministic}, SupportsArtifacts: true}, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.SerializedValidator, error) {
|
||||
probe.observe(request)
|
||||
return typedTestSerializedValidator{key: "serialized-validator"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
builders := []struct {
|
||||
name string
|
||||
call func() error
|
||||
}{
|
||||
{name: "input", call: func() error { _, err := inputs.BuildWithRequest("input", request); return err }},
|
||||
{name: "chunk", call: func() error { _, err := chunkers.BuildWithRequest("chunk", request); return err }},
|
||||
{name: "output", call: func() error { _, err := outputs.BuildWithRequest("output", request); return err }},
|
||||
{name: "extract", call: func() error {
|
||||
entry, _ := extractors.typedEntry("extract")
|
||||
_, err := entry.builder(request)
|
||||
return err
|
||||
}},
|
||||
{name: "merge", call: func() error {
|
||||
entry, _ := mergers.typedEntry("merge", "test/notes")
|
||||
_, err := entry.builder(request)
|
||||
return err
|
||||
}},
|
||||
{name: "normalize", call: func() error {
|
||||
entry, _ := normalizers.typedEntry("normalize", "test/notes")
|
||||
_, err := entry.builder(request)
|
||||
return err
|
||||
}},
|
||||
{name: "chunk validator", call: func() error {
|
||||
entry, _ := validators.chunkEntry("chunk-validator")
|
||||
_, err := entry.builder(request)
|
||||
return err
|
||||
}},
|
||||
{name: "typed validator", call: func() error {
|
||||
entry, _ := validators.typedEntry("typed-validator", "test/notes")
|
||||
_, err := entry.builder(request)
|
||||
return err
|
||||
}},
|
||||
{name: "serialized validator", call: func() error {
|
||||
entry, _ := validators.serializedEntry("serialized-validator")
|
||||
_, err := entry.builder(request)
|
||||
return err
|
||||
}},
|
||||
}
|
||||
for _, builder := range builders {
|
||||
t.Run(builder.name, func(t *testing.T) {
|
||||
if err := builder.call(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
if !reflect.DeepEqual(request, probe.want) {
|
||||
t.Fatalf("build request = %#v, want unchanged %#v", request, probe.want)
|
||||
}
|
||||
}
|
||||
|
||||
type buildRequestMutationProbe struct {
|
||||
t *testing.T
|
||||
want BuildRequest
|
||||
}
|
||||
|
||||
func (probe buildRequestMutationProbe) observe(request BuildRequest) {
|
||||
probe.t.Helper()
|
||||
if !reflect.DeepEqual(request, probe.want) {
|
||||
probe.t.Fatalf("builder request = %#v, want independently owned %#v", request, probe.want)
|
||||
}
|
||||
options := request.Options["nested"].(map[string]any)
|
||||
options["value"] = "mutated"
|
||||
request.Options["items"].([]any)[0].(map[string]any)["value"] = "mutated"
|
||||
request.Options["bytes"].([]byte)[0] = 'x'
|
||||
slot := request.References.Slots["reference"]
|
||||
slot.Items[0].Content[0] = 'x'
|
||||
slot.Items = append(slot.Items, contracts.ReferenceItem{SlotName: "reference", Content: []byte("extra")})
|
||||
request.References.Slots["reference"] = slot
|
||||
delete(request.References.Slots, "unused")
|
||||
}
|
||||
|
||||
func constructionBuildRequest() BuildRequest {
|
||||
return BuildRequest{
|
||||
Options: map[string]any{
|
||||
"nested": map[string]any{"value": "original"},
|
||||
"items": []any{map[string]any{"value": "original"}},
|
||||
"bytes": []byte("original"),
|
||||
},
|
||||
References: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
"reference": {Slot: contracts.ReferenceSlot{Name: "reference"}, Items: []contracts.ReferenceItem{{SlotName: "reference", Content: []byte("original")}}},
|
||||
"unused": {Slot: contracts.ReferenceSlot{Name: "unused"}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareFailuresOccurBeforeInputParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -100,7 +100,7 @@ func Prepare(resolved ResolvedPipeline, registries Registries, deps ModuleDepend
|
||||
artifactCodecs: registries.ArtifactCodecs,
|
||||
}
|
||||
request := func(binding ModuleBinding, references contracts.ReferenceSet) BuildRequest {
|
||||
return BuildRequest{Dependencies: deps, Options: cloneOptions(binding.Options), References: references}
|
||||
return BuildRequest{Dependencies: deps, Options: binding.Options, References: references}
|
||||
}
|
||||
|
||||
input, err := registries.Inputs.BuildWithRequest(stable.Input.Module, request(stable.Input, contracts.ReferenceSet{}))
|
||||
@@ -202,7 +202,7 @@ func prepareEvidencePlan(resolved ResolvedPipeline, registries Registries, outpu
|
||||
func prepareLane(pipeline ResolvedPipeline, lane ResolvedArtifactLane, registries Registries, deps ModuleDependencies) (preparedLaneExecutor, error) {
|
||||
executor := preparedLaneExecutor{resolved: cloneResolvedArtifactLane(lane)}
|
||||
request := func(binding ModuleBinding, references contracts.ReferenceSet) BuildRequest {
|
||||
return BuildRequest{Dependencies: deps, Options: cloneOptions(binding.Options), References: references}
|
||||
return BuildRequest{Dependencies: deps, Options: binding.Options, References: references}
|
||||
}
|
||||
extractEntry, ok := registries.Extractors.typedEntry(lane.Extract.Module)
|
||||
if !ok {
|
||||
@@ -259,7 +259,7 @@ func prepareValidatorChain(pipeline ResolvedPipeline, registries Registries, dep
|
||||
resolved := resolvedValidatorChain(stage, laneID, moduleKey, pipeline.ValidatorChains)
|
||||
prepared := preparedValidatorChain{resolved: resolved}
|
||||
for _, validator := range resolved.Validators {
|
||||
request := BuildRequest{Dependencies: deps, Options: cloneOptions(validator.Binding.Options), References: references}
|
||||
request := BuildRequest{Dependencies: deps, Options: validator.Binding.Options, References: references}
|
||||
built, err := buildPreparedValidator(registries.Validators, validator, request)
|
||||
if err != nil {
|
||||
return preparedValidatorChain{}, constructionError(pipeline.ID, laneID, stage, moduleKey, validator.Binding.Module, err)
|
||||
@@ -280,7 +280,7 @@ func buildPreparedValidator(registry *ValidatorRegistry, resolved ResolvedValida
|
||||
if !ok {
|
||||
return preparedValidator{}, fmt.Errorf("typed construction entry is not registered")
|
||||
}
|
||||
implementation, err = entry.builder(cloneBuildRequest(request))
|
||||
implementation, err = entry.builder(request)
|
||||
prepared.typed = implementation
|
||||
prepared.typedValidate = entry.validate
|
||||
case ValidatorTargetChunk:
|
||||
@@ -288,14 +288,14 @@ func buildPreparedValidator(registry *ValidatorRegistry, resolved ResolvedValida
|
||||
if !ok {
|
||||
return preparedValidator{}, fmt.Errorf("chunk construction entry is not registered")
|
||||
}
|
||||
prepared.chunk, err = entry.builder(cloneBuildRequest(request))
|
||||
prepared.chunk, err = entry.builder(request)
|
||||
implementation = prepared.chunk
|
||||
case ValidatorTargetSerialized:
|
||||
entry, ok := registry.serializedEntry(key)
|
||||
if !ok {
|
||||
return preparedValidator{}, fmt.Errorf("serialized construction entry is not registered")
|
||||
}
|
||||
prepared.serialized, err = entry.builder(cloneBuildRequest(request))
|
||||
prepared.serialized, err = entry.builder(request)
|
||||
implementation = prepared.serialized
|
||||
default:
|
||||
return preparedValidator{}, fmt.Errorf("validator construction target %q is not supported", resolved.Target)
|
||||
@@ -323,7 +323,7 @@ func buildPreparedValidator(registry *ValidatorRegistry, resolved ResolvedValida
|
||||
}
|
||||
|
||||
func buildErasedModule(builder func(BuildRequest) (any, error), request BuildRequest, key, kind string) (any, error) {
|
||||
implementation, err := builder(cloneBuildRequest(request))
|
||||
implementation, err := builder(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -150,7 +150,13 @@ func RegisterChunkValidatorBuilder(registry *ValidatorRegistry, spec ValidatorSp
|
||||
if registry.chunkEntries == nil {
|
||||
registry.chunkEntries = make(map[string]chunkValidatorEntry)
|
||||
}
|
||||
registry.chunkEntries[normalizedSpec.Key] = chunkValidatorEntry{spec: normalizedSpec, validateOptions: validateOptions, builder: builder}
|
||||
registry.chunkEntries[normalizedSpec.Key] = chunkValidatorEntry{
|
||||
spec: normalizedSpec,
|
||||
validateOptions: validateOptions,
|
||||
builder: func(request BuildRequest) (contracts.ChunkValidator, error) {
|
||||
return builder(cloneBuildRequest(request))
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -187,7 +193,13 @@ func RegisterSerializedValidatorBuilder(registry *ValidatorRegistry, spec Serial
|
||||
if registry.serializedEntries == nil {
|
||||
registry.serializedEntries = make(map[string]serializedValidatorEntry)
|
||||
}
|
||||
registry.serializedEntries[spec.Key] = serializedValidatorEntry{spec: spec, validateOptions: validateOptions, builder: builder}
|
||||
registry.serializedEntries[spec.Key] = serializedValidatorEntry{
|
||||
spec: spec,
|
||||
validateOptions: validateOptions,
|
||||
builder: func(request BuildRequest) (contracts.SerializedValidator, error) {
|
||||
return builder(cloneBuildRequest(request))
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -50,16 +50,15 @@ func (c EffectiveCatalog) Lookup(name string) (string, bool) {
|
||||
}
|
||||
|
||||
// ResolveEffectiveCatalog loads the embedded SRD catalog and applies the
|
||||
// optional spell-catalog overlay found in the cloned reference set. It does
|
||||
// not resolve paths or perform filesystem access.
|
||||
// optional spell-catalog overlay found in its spell_catalog reference slot. It
|
||||
// does not resolve paths or perform filesystem access.
|
||||
func ResolveEffectiveCatalog(references contracts.ReferenceSet) (EffectiveCatalog, error) {
|
||||
base, err := LoadSRD5E2014()
|
||||
if err != nil {
|
||||
return EffectiveCatalog{}, err
|
||||
}
|
||||
|
||||
cloned := cloneReferenceSet(references)
|
||||
slot, ok := cloned.Slots[SpellCatalogReferenceSlot]
|
||||
slot, ok := references.Slots[SpellCatalogReferenceSlot]
|
||||
if !ok || len(slot.Items) == 0 {
|
||||
return composeEffectiveCatalog(base, nil)
|
||||
}
|
||||
@@ -407,21 +406,3 @@ func cloneStringMap(values map[string]string) map[string]string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneReferenceSet(in contracts.ReferenceSet) contracts.ReferenceSet {
|
||||
if len(in.Slots) == 0 {
|
||||
return contracts.ReferenceSet{}
|
||||
}
|
||||
out := contracts.ReferenceSet{Slots: make(map[string]contracts.ResolvedReferenceSlot, len(in.Slots))}
|
||||
for name, slot := range in.Slots {
|
||||
slot.Slot.AcceptedMediaTypes = append([]string(nil), slot.Slot.AcceptedMediaTypes...)
|
||||
items := make([]contracts.ReferenceItem, len(slot.Items))
|
||||
for index, item := range slot.Items {
|
||||
item.Content = append([]byte(nil), item.Content...)
|
||||
items[index] = item
|
||||
}
|
||||
slot.Items = items
|
||||
out.Slots[name] = slot
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user