From ce857966f1085e6dee99648ec882d30b4e8cb3ff Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 3 Aug 2026 16:49:51 +0000 Subject: [PATCH] Add execution metadata to module specifications --- docs/internal/dnd.md | 6 +- docs/internal/modules.md | 18 ++- internal/cli/production_contract_test.go | 29 ++++ internal/core/config/redaction_test.go | 41 +++-- .../pipeline/chunker_registry_test.go | 11 +- .../framework/pipeline/input_registry_test.go | 11 +- internal/framework/pipeline/module.go | 7 + internal/framework/pipeline/module_test.go | 13 ++ internal/framework/pipeline/profile.go | 140 +++++++++++++----- internal/framework/pipeline/profile_test.go | 57 ++++++- internal/modules/dnd/chunk/scenes/chunker.go | 1 + .../modules/dnd/chunk/scenes/chunker_test.go | 1 + .../dnd/extract/combatturns/extractor.go | 1 + .../dnd/extract/itemevents/extractor.go | 1 + .../dnd/extract/itemevents/registry_test.go | 2 +- .../dnd/extract/npcinteractions/extractor.go | 1 + .../modules/dnd/extract/npcs/extractor.go | 1 + .../modules/dnd/extract/npcs/registry_test.go | 11 +- .../extract/scenedescriptions/extractor.go | 1 + .../scenedescriptions/registry_test.go | 2 +- .../modules/dnd/extract/spells/extractor.go | 1 + .../dnd/extract/spells/registry_test.go | 5 +- .../dnd/normalize/combatturns/normalizer.go | 1 + .../dnd/normalize/itemevents/normalizer.go | 11 +- .../normalize/npcinteractions/normalizer.go | 1 + .../modules/dnd/normalize/npcs/normalizer.go | 2 +- .../dnd/normalize/npcs/normalizer_test.go | 2 +- .../normalize/scenedescriptions/normalizer.go | 11 +- .../scenedescriptions/normalizer_test.go | 2 +- .../dnd/normalize/spells/normalizer.go | 1 + .../dnd/normalize/spells/normalizer_test.go | 11 +- .../modules/generic/chunk/units/chunker.go | 7 +- .../generic/chunk/units/chunker_test.go | 7 +- .../generic/merge/appendorder/merger.go | 3 +- .../generic/normalize/noop/normalizer.go | 3 +- .../modules/generic/output/json/encoder.go | 9 +- .../generic/output/json/encoder_test.go | 9 +- .../seriatim/input/transcript/adapter.go | 7 +- .../input/transcript/registry_test.go | 6 +- 39 files changed, 331 insertions(+), 123 deletions(-) diff --git a/docs/internal/dnd.md b/docs/internal/dnd.md index 5c5c56ba..62268b9b 100644 --- a/docs/internal/dnd.md +++ b/docs/internal/dnd.md @@ -24,7 +24,11 @@ does not repeat their JSON shapes or schemas. The D&D registrar registers the family’s artifact codecs, extractors, typed append-order mergers, normalizers, validators, prompt assets, fallback LLM profile asset, and default validator chains. Each extractor and normalizer has -a stable module spec, strict option decoding, and a typed builder. +a stable module spec, explicit execution class, strict option decoding, and a +typed builder. Scene chunking, every extractor, and NPC normalization are +registered as `llm_backed`; the remaining current D&D mergers and normalizers +are `deterministic`. The metadata is available to catalog inspection and +resolved-pipeline debug data; it does not yet change profile inheritance. Configuration remains the canonical owner of the exact keys and validator order. diff --git a/docs/internal/modules.md b/docs/internal/modules.md index 9586e889..3f70f218 100644 --- a/docs/internal/modules.md +++ b/docs/internal/modules.md @@ -12,9 +12,13 @@ exceptions. See [D&D Module Internals](dnd.md) rather than adding them here. A module is a typed implementation registered for one pipeline stage. Its `ModuleSpec` is the public-to-the-framework declaration of its stable key, -stage, required and provided capabilities, artifact kind, and accepted -reference slots. The framework uses that declaration to resolve a configured -binding before it builds the implementation. +stage, execution class, required and provided capabilities, artifact kind, and +accepted reference slots. The execution class states whether a module is +`deterministic` or `llm_backed`; registries retain it for catalog inspection and +resolved-pipeline debug data without constructing the module. The framework +uses the declaration to resolve a configured binding before it builds the +implementation. Profile inheritance and deterministic-profile validation are +not implemented at this boundary yet. Implementations that accept options must provide both an option validator and a builder. The validator is used while resolving configuration; the builder @@ -59,10 +63,10 @@ packages depend on production extensions. 1. Choose the pipeline stage and the typed artifact boundary. Put external input or durable artifact formats in the relevant integration contract, not in this guide or in a private LLM response type. -2. Define a stable `ModuleSpec` with the exact capabilities and reference - slots needed for the operation. Model a producer/consumer handoff as an - artifact-compatible slot; configuration then chooses an external file or a - generated binding. +2. Define a stable `ModuleSpec` with an explicit execution class, the exact + capabilities, and reference slots needed for the operation. Model a + producer/consumer handoff as an artifact-compatible slot; configuration + then chooses an external file or a generated binding. 3. Implement strict option decoding, construction, and the typed stage interface. Preserve caller ownership: do not retain mutable request data and return defensive copies where an implementation exposes stored data. diff --git a/internal/cli/production_contract_test.go b/internal/cli/production_contract_test.go index 04070fb5..ec05075a 100644 --- a/internal/cli/production_contract_test.go +++ b/internal/cli/production_contract_test.go @@ -166,6 +166,35 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) { assertProductionContains(t, "production prompt assets", assetNames, requiredAssets) catalog := catalogFromRegistries(registries) + for _, test := range []struct { + stage pipeline.ModuleStage + key string + want contracts.ExecutionClass + }{ + {stage: pipeline.StageInput, key: "seriatim", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageChunk, key: "generic", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageChunk, key: "dnd/scenes", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageExtract, key: "dnd/spells", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageExtract, key: "dnd/npcs", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageExtract, key: "dnd/combat-turns", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageExtract, key: "dnd/item-events", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageExtract, key: "dnd/npc-interactions", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageExtract, key: "dnd/scene-descriptions", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageMerge, key: "appendorder", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageNormalize, key: "noop", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageNormalize, key: "dnd/spells", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageNormalize, key: "dnd/npcs", want: contracts.ExecutionClassLLMBacked}, + {stage: pipeline.StageNormalize, key: "dnd/combat-turns", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageNormalize, key: "dnd/item-events", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageNormalize, key: "dnd/npc-interactions", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageNormalize, key: "dnd/scene-descriptions", want: contracts.ExecutionClassDeterministic}, + {stage: pipeline.StageOutput, key: "json", want: contracts.ExecutionClassDeterministic}, + } { + got, ok := catalog.ExecutionClass(test.stage, test.key) + if !ok || got != test.want { + t.Fatalf("production execution class for %s/%s = %q, %t; want %q, true", test.stage, test.key, got, ok, test.want) + } + } converted := registriesFromCatalog(catalog) if converted.ArtifactCodecs != registries.ArtifactCodecs || converted.ArtifactEvidence != registries.ArtifactEvidence || converted.ValidatorChains != registries.ValidatorChains { t.Fatal("catalog/registry conversion did not preserve artifact and validator registries") diff --git a/internal/core/config/redaction_test.go b/internal/core/config/redaction_test.go index 4029406a..1ad75651 100644 --- a/internal/core/config/redaction_test.go +++ b/internal/core/config/redaction_test.go @@ -19,23 +19,28 @@ func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) { } resolved := pipeline.ResolvedPipeline{ - ID: "redaction-test", - Digest: "sha256:safe-digest", - Input: bindings["input"], - Chunk: bindings["chunk"], - ChunkReferences: redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"), + ID: "redaction-test", + Digest: "sha256:safe-digest", + Input: bindings["input"], + InputExecutionClass: contracts.ExecutionClassDeterministic, + Chunk: bindings["chunk"], + ChunkExecutionClass: contracts.ExecutionClassLLMBacked, + ChunkReferences: redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"), Steps: []pipeline.ResolvedPipelineStep{{ ID: "default", ArtifactLanes: []pipeline.ResolvedArtifactLane{{ - ID: "safe-lane", - ArtifactKind: "safe/artifact", - Extract: bindings["extract"], - Merge: bindings["merge"], - Normalize: bindings["normalize"], - Validators: []pipeline.ModuleBinding{bindings["lane-validator"]}, - ExtractReferences: redactionTestReferenceTarget(pipeline.StageExtract, "safe-lane", "extract-reference-content"), - MergeReferences: redactionTestReferenceTarget(pipeline.StageMerge, "safe-lane", "merge-reference-content"), - NormalizeReferences: redactionTestReferenceTarget(pipeline.StageNormalize, "safe-lane", "normalize-reference-content"), + ID: "safe-lane", + ArtifactKind: "safe/artifact", + Extract: bindings["extract"], + ExtractExecutionClass: contracts.ExecutionClassLLMBacked, + Merge: bindings["merge"], + MergeExecutionClass: contracts.ExecutionClassDeterministic, + Normalize: bindings["normalize"], + NormalizeExecutionClass: contracts.ExecutionClassLLMBacked, + Validators: []pipeline.ModuleBinding{bindings["lane-validator"]}, + ExtractReferences: redactionTestReferenceTarget(pipeline.StageExtract, "safe-lane", "extract-reference-content"), + MergeReferences: redactionTestReferenceTarget(pipeline.StageMerge, "safe-lane", "merge-reference-content"), + NormalizeReferences: redactionTestReferenceTarget(pipeline.StageNormalize, "safe-lane", "normalize-reference-content"), }}, }}, ValidatorChains: []pipeline.ResolvedValidatorChain{{ @@ -49,7 +54,8 @@ func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) { ArtifactKind: "safe/artifact", }}, }}, - Output: bindings["output"], + Output: bindings["output"], + OutputExecutionClass: contracts.ExecutionClassDeterministic, } effective := EffectiveConfig{ Config: Config{Pipelines: map[string]pipeline.PipelineProfile{ @@ -88,6 +94,11 @@ func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) { t.Fatalf("resolved pipeline summary does not retain %q: %s", safe, text) } } + for _, executionClass := range []string{"input_execution_class\":\"deterministic", "chunk_execution_class\":\"llm_backed", "extract_execution_class\":\"llm_backed", "merge_execution_class\":\"deterministic", "normalize_execution_class\":\"llm_backed", "output_execution_class\":\"deterministic"} { + if !strings.Contains(text, executionClass) { + t.Fatalf("resolved pipeline summary does not retain %q: %s", executionClass, text) + } + } payload.Input.Options["safe"] = "mutated" nested := payload.Input.Options["nested"].([]any)[0].([]any)[0].(map[string]any) diff --git a/internal/framework/pipeline/chunker_registry_test.go b/internal/framework/pipeline/chunker_registry_test.go index 4c6640e6..56934c7f 100644 --- a/internal/framework/pipeline/chunker_registry_test.go +++ b/internal/framework/pipeline/chunker_registry_test.go @@ -115,10 +115,11 @@ func runRegistryBehaviorTests[M any](t *testing.T, testCase registryBehaviorCase t.Fatal("Spec() ok = false, want true") } want := ModuleSpec{ - Key: testCase.key, - Stage: testCase.stage, - Provides: []string{"alpha", "beta"}, - Requires: []string{"source"}, + Key: testCase.key, + Stage: testCase.stage, + ExecutionClass: contracts.ExecutionClassDeterministic, + Provides: []string{"alpha", "beta"}, + Requires: []string{"source"}, } if !reflect.DeepEqual(got, want) { t.Fatalf("Spec() = %#v, want %#v", got, want) @@ -144,7 +145,7 @@ func runRegistryBehaviorTests[M any](t *testing.T, testCase registryBehaviorCase if !ok { t.Fatal("Spec() ok = false, want true") } - want := ModuleSpec{Key: testCase.key, Stage: testCase.stage} + want := ModuleSpec{Key: testCase.key, Stage: testCase.stage, ExecutionClass: contracts.ExecutionClassDeterministic} if !reflect.DeepEqual(spec, want) { t.Fatalf("Spec() = %#v, want %#v", spec, want) } diff --git a/internal/framework/pipeline/input_registry_test.go b/internal/framework/pipeline/input_registry_test.go index e995504a..f03c09cb 100644 --- a/internal/framework/pipeline/input_registry_test.go +++ b/internal/framework/pipeline/input_registry_test.go @@ -61,10 +61,11 @@ func TestInputAdapterRegistryRegisterWithSpecStoresMetadata(t *testing.T) { t.Fatal("Spec() ok = false, want true") } want := ModuleSpec{ - Key: "generic-input", - Stage: StageInput, - Provides: []string{"parsed-source", "source-document"}, - Requires: []string{"raw-bytes"}, + Key: "generic-input", + Stage: StageInput, + ExecutionClass: contracts.ExecutionClassDeterministic, + Provides: []string{"parsed-source", "source-document"}, + Requires: []string{"raw-bytes"}, } if !reflect.DeepEqual(got, want) { t.Fatalf("Spec() = %#v, want %#v", got, want) @@ -91,7 +92,7 @@ func TestInputAdapterRegistryRegisterStoresDefaultSpec(t *testing.T) { if !ok { t.Fatal("Spec() ok = false, want true") } - want := ModuleSpec{Key: "generic-input", Stage: StageInput} + want := ModuleSpec{Key: "generic-input", Stage: StageInput, ExecutionClass: contracts.ExecutionClassDeterministic} if !reflect.DeepEqual(got, want) { t.Fatalf("Spec() = %#v, want %#v", got, want) } diff --git a/internal/framework/pipeline/module.go b/internal/framework/pipeline/module.go index 1bbb4e7b..2d29a2e6 100644 --- a/internal/framework/pipeline/module.go +++ b/internal/framework/pipeline/module.go @@ -23,6 +23,7 @@ const ( type ModuleSpec struct { Key string Stage ModuleStage + ExecutionClass contracts.ExecutionClass ArtifactKind contracts.ArtifactKind Provides []string Requires []string @@ -37,9 +38,14 @@ func defaultModuleSpec(key string, stage ModuleStage) ModuleSpec { } func normalizeModuleSpec(spec ModuleSpec) ModuleSpec { + executionClass := contracts.ExecutionClass(strings.TrimSpace(string(spec.ExecutionClass))) + if executionClass == "" { + executionClass = contracts.ExecutionClassDeterministic + } return ModuleSpec{ Key: strings.TrimSpace(spec.Key), Stage: spec.Stage, + ExecutionClass: executionClass, ArtifactKind: normalizeArtifactKind(spec.ArtifactKind), Provides: normalizeCapabilities(spec.Provides), Requires: normalizeCapabilities(spec.Requires), @@ -76,6 +82,7 @@ func cloneModuleSpec(spec ModuleSpec) ModuleSpec { return ModuleSpec{ Key: spec.Key, Stage: spec.Stage, + ExecutionClass: spec.ExecutionClass, ArtifactKind: spec.ArtifactKind, Provides: append([]string(nil), spec.Provides...), Requires: append([]string(nil), spec.Requires...), diff --git a/internal/framework/pipeline/module_test.go b/internal/framework/pipeline/module_test.go index acb6b3d2..2b98f174 100644 --- a/internal/framework/pipeline/module_test.go +++ b/internal/framework/pipeline/module_test.go @@ -1,12 +1,25 @@ package pipeline import ( + "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) +func TestNormalizeModuleSpecDefaultsExecutionClass(t *testing.T) { + normalized := normalizeModuleSpec(ModuleSpec{Key: " module ", Stage: StageChunk}) + if normalized.ExecutionClass != contracts.ExecutionClassDeterministic { + t.Fatalf("ExecutionClass = %q, want deterministic compatibility default", normalized.ExecutionClass) + } + + cloned := cloneModuleSpec(normalized) + if !reflect.DeepEqual(cloned, normalized) { + t.Fatalf("cloneModuleSpec() = %#v, want %#v", cloned, normalized) + } +} + func TestValidateModuleSpecAllowsReferenceSlotsForEligibleStages(t *testing.T) { tests := []struct { name string diff --git a/internal/framework/pipeline/profile.go b/internal/framework/pipeline/profile.go index e8c788de..ea1bc2bb 100644 --- a/internal/framework/pipeline/profile.go +++ b/internal/framework/pipeline/profile.go @@ -156,20 +156,23 @@ type ResolvedReferenceTarget struct { } type ResolvedArtifactLane struct { - StepID string - ID string - ArtifactKind contracts.ArtifactKind `json:"artifact_kind,omitempty"` - ArtifactSchemaID string `json:"artifact_schema_id,omitempty"` - ArtifactSchemaName string `json:"artifact_schema_name,omitempty"` - ArtifactSchemaVersion string `json:"artifact_schema_version,omitempty"` - ArtifactSchemaDigest string `json:"artifact_schema_digest,omitempty"` - Extract ModuleBinding - Merge ModuleBinding - Normalize ModuleBinding - Validators []ModuleBinding - ExtractReferences ResolvedReferenceTarget `json:"extract_references"` - MergeReferences ResolvedReferenceTarget `json:"merge_references"` - NormalizeReferences ResolvedReferenceTarget `json:"normalize_references"` + StepID string + ID string + ArtifactKind contracts.ArtifactKind `json:"artifact_kind,omitempty"` + ArtifactSchemaID string `json:"artifact_schema_id,omitempty"` + ArtifactSchemaName string `json:"artifact_schema_name,omitempty"` + ArtifactSchemaVersion string `json:"artifact_schema_version,omitempty"` + ArtifactSchemaDigest string `json:"artifact_schema_digest,omitempty"` + Extract ModuleBinding + ExtractExecutionClass contracts.ExecutionClass `json:"extract_execution_class"` + Merge ModuleBinding + MergeExecutionClass contracts.ExecutionClass `json:"merge_execution_class"` + Normalize ModuleBinding + NormalizeExecutionClass contracts.ExecutionClass `json:"normalize_execution_class"` + Validators []ModuleBinding + ExtractReferences ResolvedReferenceTarget `json:"extract_references"` + MergeReferences ResolvedReferenceTarget `json:"merge_references"` + NormalizeReferences ResolvedReferenceTarget `json:"normalize_references"` } type ResolvedPipelineStep struct { @@ -192,14 +195,17 @@ type ResolvedValidator struct { } type ResolvedPipeline struct { - ID string - Digest string - Input ModuleBinding - Chunk ModuleBinding - ChunkReferences ResolvedReferenceTarget `json:"chunk_references"` - Steps []ResolvedPipelineStep - ValidatorChains []ResolvedValidatorChain `json:"validator_chains"` - Output ModuleBinding + ID string + Digest string + Input ModuleBinding + InputExecutionClass contracts.ExecutionClass `json:"input_execution_class"` + Chunk ModuleBinding + ChunkExecutionClass contracts.ExecutionClass `json:"chunk_execution_class"` + ChunkReferences ResolvedReferenceTarget `json:"chunk_references"` + Steps []ResolvedPipelineStep + ValidatorChains []ResolvedValidatorChain `json:"validator_chains"` + Output ModuleBinding + OutputExecutionClass contracts.ExecutionClass `json:"output_execution_class"` } // AllArtifactLanes returns lanes in deterministic step order for read-only @@ -225,6 +231,46 @@ type ModuleCatalog struct { Outputs *OutputEncoderRegistry } +// ExecutionClass returns the registered execution class for a module selected +// by stage and key without constructing the module. +func (catalog ModuleCatalog) ExecutionClass(stage ModuleStage, key string) (contracts.ExecutionClass, bool) { + var executionClass contracts.ExecutionClass + var ok bool + + switch stage { + case StageInput: + var spec ModuleSpec + spec, ok = catalog.Inputs.Spec(key) + executionClass = spec.ExecutionClass + case StageChunk: + var spec ModuleSpec + spec, ok = catalog.Chunkers.Spec(key) + executionClass = spec.ExecutionClass + case StageExtract: + var spec ModuleSpec + spec, ok = catalog.Extractors.Spec(key) + executionClass = spec.ExecutionClass + case StageMerge: + var spec ModuleSpec + spec, ok = catalog.Mergers.Spec(key) + executionClass = spec.ExecutionClass + case StageNormalize: + var spec ModuleSpec + spec, ok = catalog.Normalizers.Spec(key) + executionClass = spec.ExecutionClass + case StageValidate: + var spec ValidatorSpec + spec, ok = catalog.Validators.Spec(key) + executionClass = spec.ExecutionClass + case StageOutput: + var spec ModuleSpec + spec, ok = catalog.Outputs.Spec(key) + executionClass = spec.ExecutionClass + } + + return executionClass, ok +} + func Binding(module string) ModuleBinding { return ModuleBinding{Module: strings.TrimSpace(module)} } @@ -316,11 +362,13 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo return ResolvedPipeline{}, err } resolved := ResolvedPipeline{ - ID: pipelineID, - Input: input, - Chunk: chunk, - ChunkReferences: referenceTarget(StageChunk, "", chunk.Module, chunkReferences), - Output: resolveBinding(profile.Output, DefaultOutputModule), + ID: pipelineID, + Input: input, + InputExecutionClass: inputModuleSpec.ExecutionClass, + Chunk: chunk, + ChunkExecutionClass: chunkSpec.ExecutionClass, + ChunkReferences: referenceTarget(StageChunk, "", chunk.Module, chunkReferences), + Output: resolveBinding(profile.Output, DefaultOutputModule), } chunkValidatorChain, err := resolveValidatorChain(pipelineID, "", StageChunk, chunk.Module, chunk.Validators, "", nil, catalog) if err != nil { @@ -386,6 +434,7 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo if missing, ok := outputCapabilities.missing(outputSpec.Requires); ok { return ResolvedPipeline{}, capabilityError(pipelineID, "", StageOutput, resolved.Output.Module, missing) } + resolved.OutputExecutionClass = outputSpec.ExecutionClass if err := validateResolvedOptions(resolved, catalog, configuredLaneIDs); err != nil { return ResolvedPipeline{}, err } @@ -475,6 +524,7 @@ func resolveArtifactLane( } lane.ExtractReferences = referenceTarget(StageExtract, laneID, lane.Extract.Module, references) lane.ExtractReferences.StepID = strings.TrimSpace(stepID) + lane.ExtractExecutionClass = extractSpec.ExecutionClass capabilities.add(extractSpec.Provides...) mergeSpec, err := mergerSpecForArtifact(catalog, lane.Merge.Module, lane.ArtifactKind, artifactType) @@ -499,6 +549,7 @@ func resolveArtifactLane( } lane.MergeReferences = referenceTarget(StageMerge, laneID, lane.Merge.Module, mergeReferences) lane.MergeReferences.StepID = strings.TrimSpace(stepID) + lane.MergeExecutionClass = mergeSpec.ExecutionClass capabilities.add(mergeSpec.Provides...) normalizeSpec, err := normalizerSpecForArtifact(catalog, lane.Normalize.Module, lane.ArtifactKind, artifactType) @@ -523,6 +574,7 @@ func resolveArtifactLane( } lane.NormalizeReferences = referenceTarget(StageNormalize, laneID, lane.Normalize.Module, normalizeReferences) lane.NormalizeReferences.StepID = strings.TrimSpace(stepID) + lane.NormalizeExecutionClass = normalizeSpec.ExecutionClass capabilities.add(normalizeSpec.Provides...) if len(lane.Validators) > 0 { @@ -1307,21 +1359,27 @@ func selectedArtifactLanes(pipelineID string, artifacts map[string]ArtifactLaneP func resolvedPipelineDigest(resolved ResolvedPipeline) (string, error) { withoutDigest := struct { - ID string - Input ModuleBinding - Chunk ModuleBinding - ChunkReferences ResolvedReferenceTarget - Steps []ResolvedPipelineStep - ValidatorChains []ResolvedValidatorChain - Output ModuleBinding + ID string + Input ModuleBinding + InputExecutionClass contracts.ExecutionClass + Chunk ModuleBinding + ChunkExecutionClass contracts.ExecutionClass + ChunkReferences ResolvedReferenceTarget + Steps []ResolvedPipelineStep + ValidatorChains []ResolvedValidatorChain + Output ModuleBinding + OutputExecutionClass contracts.ExecutionClass }{ - ID: resolved.ID, - Input: resolved.Input, - Chunk: resolved.Chunk, - ChunkReferences: resolved.ChunkReferences, - Steps: resolved.Steps, - ValidatorChains: resolved.ValidatorChains, - Output: resolved.Output, + ID: resolved.ID, + Input: resolved.Input, + InputExecutionClass: resolved.InputExecutionClass, + Chunk: resolved.Chunk, + ChunkExecutionClass: resolved.ChunkExecutionClass, + ChunkReferences: resolved.ChunkReferences, + Steps: resolved.Steps, + ValidatorChains: resolved.ValidatorChains, + Output: resolved.Output, + OutputExecutionClass: resolved.OutputExecutionClass, } encoded, err := json.Marshal(withoutDigest) if err != nil { diff --git a/internal/framework/pipeline/profile_test.go b/internal/framework/pipeline/profile_test.go index a10345b0..1df59b2f 100644 --- a/internal/framework/pipeline/profile_test.go +++ b/internal/framework/pipeline/profile_test.go @@ -47,12 +47,18 @@ func TestResolvePipelineWithExplicitModules(t *testing.T) { if !reflect.DeepEqual(resolved.Input, ModuleBinding{Module: "text", LLMProfile: "fast"}) { t.Fatalf("Input = %#v, want trimmed explicit input", resolved.Input) } + if resolved.InputExecutionClass != contracts.ExecutionClassDeterministic { + t.Fatalf("InputExecutionClass = %q, want deterministic", resolved.InputExecutionClass) + } if resolved.Chunk.Module != "window" || resolved.Chunk.LLMProfile != "" { t.Fatalf("Chunk = %#v, want explicit module and empty LLM profile", resolved.Chunk) } if resolved.Chunk.Options["size"] != 10 { t.Fatalf("Chunk.Options = %#v, want size option", resolved.Chunk.Options) } + if resolved.ChunkExecutionClass != contracts.ExecutionClassDeterministic { + t.Fatalf("ChunkExecutionClass = %q, want deterministic", resolved.ChunkExecutionClass) + } if len(resolved.Steps) != 1 || resolved.Steps[0].ID != "default" || len(resolved.Steps[0].ArtifactLanes) != 1 { t.Fatalf("resolved steps = %#v, want one default step with one lane", resolved.Steps) } @@ -63,6 +69,9 @@ func TestResolvePipelineWithExplicitModules(t *testing.T) { if !reflect.DeepEqual(lane.Extract, ModuleBinding{Module: "record-extractor", LLMProfile: "careful"}) { t.Fatalf("lane.Extract = %#v, want explicit extractor", lane.Extract) } + if lane.ExtractExecutionClass != contracts.ExecutionClassDeterministic || lane.MergeExecutionClass != contracts.ExecutionClassDeterministic || lane.NormalizeExecutionClass != contracts.ExecutionClassDeterministic { + t.Fatalf("lane execution classes = %q/%q/%q, want deterministic", lane.ExtractExecutionClass, lane.MergeExecutionClass, lane.NormalizeExecutionClass) + } if lane.Merge.Module != "dedupe" || lane.Normalize.Module != "canonical" { t.Fatalf("lane merge/normalize = %#v/%#v, want explicit modules", lane.Merge, lane.Normalize) } @@ -72,11 +81,51 @@ func TestResolvePipelineWithExplicitModules(t *testing.T) { if resolved.Output.Module != "ndjson" { t.Fatalf("Output.Module = %q, want ndjson", resolved.Output.Module) } + if resolved.OutputExecutionClass != contracts.ExecutionClassDeterministic { + t.Fatalf("OutputExecutionClass = %q, want deterministic", resolved.OutputExecutionClass) + } if !strings.HasPrefix(resolved.Digest, "sha256:") { t.Fatalf("Digest = %q, want sha256 digest", resolved.Digest) } } +func TestModuleCatalogExecutionClassLooksUpRegisteredMetadata(t *testing.T) { + catalog := newProfileCatalogWithOverrides(t, + ModuleSpec{Key: "llm-input", Stage: StageInput, ExecutionClass: contracts.ExecutionClassLLMBacked}, + ModuleSpec{Key: "llm-chunk", Stage: StageChunk, ExecutionClass: contracts.ExecutionClassLLMBacked}, + ModuleSpec{Key: "llm-extract", Stage: StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked}, + ModuleSpec{Key: "llm-merge", Stage: StageMerge, ExecutionClass: contracts.ExecutionClassLLMBacked}, + ModuleSpec{Key: "llm-normalize", Stage: StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked}, + ModuleSpec{Key: "llm-output", Stage: StageOutput, ExecutionClass: contracts.ExecutionClassLLMBacked}, + ) + registerProfileValidatorSpec(t, catalog, ValidatorSpec{Key: "llm-validator", ExecutionClass: contracts.ExecutionClassLLMBacked}) + + for _, test := range []struct { + stage ModuleStage + key string + want contracts.ExecutionClass + }{ + {stage: StageInput, key: "llm-input", want: contracts.ExecutionClassLLMBacked}, + {stage: StageChunk, key: "llm-chunk", want: contracts.ExecutionClassLLMBacked}, + {stage: StageExtract, key: "llm-extract", want: contracts.ExecutionClassLLMBacked}, + {stage: StageMerge, key: "llm-merge", want: contracts.ExecutionClassLLMBacked}, + {stage: StageNormalize, key: "llm-normalize", want: contracts.ExecutionClassLLMBacked}, + {stage: StageValidate, key: "llm-validator", want: contracts.ExecutionClassLLMBacked}, + {stage: StageOutput, key: "llm-output", want: contracts.ExecutionClassLLMBacked}, + } { + t.Run(string(test.stage), func(t *testing.T) { + got, ok := catalog.ExecutionClass(test.stage, test.key) + if !ok || got != test.want { + t.Fatalf("ExecutionClass(%q, %q) = %q, %t; want %q, true", test.stage, test.key, got, ok, test.want) + } + }) + } + + if _, ok := catalog.ExecutionClass(StageExtract, "missing"); ok { + t.Fatal("ExecutionClass() found an unregistered module") + } +} + func TestResolvePipelineAppliesDefaults(t *testing.T) { resolved, err := ResolvePipeline(PipelineProfile{ ID: "defaulted", @@ -1566,7 +1615,13 @@ func TestResolvedPipelineCanMarshalToCanonicalJSON(t *testing.T) { if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } - if _, err := json.Marshal(resolved); err != nil { + encoded, err := json.Marshal(resolved) + if err != nil { t.Fatalf("json.Marshal(resolved) error = %v, want nil", err) } + for _, field := range []string{"input_execution_class", "chunk_execution_class", "extract_execution_class", "merge_execution_class", "normalize_execution_class", "output_execution_class"} { + if !strings.Contains(string(encoded), `"`+field+`":"deterministic"`) { + t.Fatalf("resolved JSON does not retain %q: %s", field, encoded) + } + } } diff --git a/internal/modules/dnd/chunk/scenes/chunker.go b/internal/modules/dnd/chunk/scenes/chunker.go index 61a71fe8..7005d6f5 100644 --- a/internal/modules/dnd/chunk/scenes/chunker.go +++ b/internal/modules/dnd/chunk/scenes/chunker.go @@ -120,6 +120,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageChunk, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions), diff --git a/internal/modules/dnd/chunk/scenes/chunker_test.go b/internal/modules/dnd/chunk/scenes/chunker_test.go index 1df9cbad..24ab6b73 100644 --- a/internal/modules/dnd/chunk/scenes/chunker_test.go +++ b/internal/modules/dnd/chunk/scenes/chunker_test.go @@ -24,6 +24,7 @@ func TestNewModuleSpecAndRegister(t *testing.T) { want := pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageChunk, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"source.transcript"}, Provides: []string{"chunks"}, ReferenceSlots: wantReferenceSlots(), diff --git a/internal/modules/dnd/extract/combatturns/extractor.go b/internal/modules/dnd/extract/combatturns/extractor.go index 98e891a6..f64483c0 100644 --- a/internal/modules/dnd/extract/combatturns/extractor.go +++ b/internal/modules/dnd/extract/combatturns/extractor.go @@ -226,6 +226,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.CombatTurnListKind, diff --git a/internal/modules/dnd/extract/itemevents/extractor.go b/internal/modules/dnd/extract/itemevents/extractor.go index 280c17ac..03e42d18 100644 --- a/internal/modules/dnd/extract/itemevents/extractor.go +++ b/internal/modules/dnd/extract/itemevents/extractor.go @@ -129,6 +129,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.ItemEventListKind, diff --git a/internal/modules/dnd/extract/itemevents/registry_test.go b/internal/modules/dnd/extract/itemevents/registry_test.go index 640ecbfb..5aed4e3b 100644 --- a/internal/modules/dnd/extract/itemevents/registry_test.go +++ b/internal/modules/dnd/extract/itemevents/registry_test.go @@ -18,7 +18,7 @@ func TestConstructorSpecOptionsAndMetadata(t *testing.T) { t.Fatalf("New() error = %v", err) } want := pipeline.ModuleSpec{ - Key: Key, Stage: pipeline.StageExtract, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.item_events"}, ArtifactKind: dnd.ItemEventListKind, + Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.item_events"}, ArtifactKind: dnd.ItemEventListKind, ReferenceSlots: []contracts.ReferenceSlot{ {Name: "glossary", Description: referenceSlotDescriptions.Glossary, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}}, {Name: "party", Description: referenceSlotDescriptions.Party, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}}, diff --git a/internal/modules/dnd/extract/npcinteractions/extractor.go b/internal/modules/dnd/extract/npcinteractions/extractor.go index 0a5091a6..5c01564f 100644 --- a/internal/modules/dnd/extract/npcinteractions/extractor.go +++ b/internal/modules/dnd/extract/npcinteractions/extractor.go @@ -177,6 +177,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCInteractionListKind, diff --git a/internal/modules/dnd/extract/npcs/extractor.go b/internal/modules/dnd/extract/npcs/extractor.go index 886a11df..e6f02ea0 100644 --- a/internal/modules/dnd/extract/npcs/extractor.go +++ b/internal/modules/dnd/extract/npcs/extractor.go @@ -135,6 +135,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCListKind, diff --git a/internal/modules/dnd/extract/npcs/registry_test.go b/internal/modules/dnd/extract/npcs/registry_test.go index 9cab7f46..bd81431f 100644 --- a/internal/modules/dnd/extract/npcs/registry_test.go +++ b/internal/modules/dnd/extract/npcs/registry_test.go @@ -25,11 +25,12 @@ func TestNewRequiresLLMClientAndRejectsAmbiguousReferences(t *testing.T) { func TestModuleSpecAndReferenceSlots(t *testing.T) { got := ModuleSpec() want := pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageExtract, - Requires: []string{"chunks", "source.transcript"}, - Provides: []string{"dnd.npcs"}, - ArtifactKind: dnd.NPCListKind, + Key: Key, + Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, + Requires: []string{"chunks", "source.transcript"}, + Provides: []string{"dnd.npcs"}, + ArtifactKind: dnd.NPCListKind, ReferenceSlots: []contracts.ReferenceSlot{ {Name: "glossary", Description: "Optional campaign glossary reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}}, {Name: "party", Description: "Optional party roster reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}}, diff --git a/internal/modules/dnd/extract/scenedescriptions/extractor.go b/internal/modules/dnd/extract/scenedescriptions/extractor.go index 9ce70013..6188fd1d 100644 --- a/internal/modules/dnd/extract/scenedescriptions/extractor.go +++ b/internal/modules/dnd/extract/scenedescriptions/extractor.go @@ -146,6 +146,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.SceneDescriptionListKind, diff --git a/internal/modules/dnd/extract/scenedescriptions/registry_test.go b/internal/modules/dnd/extract/scenedescriptions/registry_test.go index 547c5994..d717c86f 100644 --- a/internal/modules/dnd/extract/scenedescriptions/registry_test.go +++ b/internal/modules/dnd/extract/scenedescriptions/registry_test.go @@ -24,7 +24,7 @@ func TestNewRequiresLLMClientAndRejectsAmbiguousReferences(t *testing.T) { func TestModuleSpecAndReferenceSlots(t *testing.T) { want := pipeline.ModuleSpec{ - Key: Key, Stage: pipeline.StageExtract, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.scene_descriptions"}, ArtifactKind: dnd.SceneDescriptionListKind, + Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.scene_descriptions"}, ArtifactKind: dnd.SceneDescriptionListKind, ReferenceSlots: []contracts.ReferenceSlot{ {Name: "glossary", Description: "Optional campaign glossary reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}}, {Name: "party", Description: "Optional party roster reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}}, diff --git a/internal/modules/dnd/extract/spells/extractor.go b/internal/modules/dnd/extract/spells/extractor.go index d6094933..db5cdf0a 100644 --- a/internal/modules/dnd/extract/spells/extractor.go +++ b/internal/modules/dnd/extract/spells/extractor.go @@ -200,6 +200,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.SpellListKind, diff --git a/internal/modules/dnd/extract/spells/registry_test.go b/internal/modules/dnd/extract/spells/registry_test.go index e2271bae..2e944ae0 100644 --- a/internal/modules/dnd/extract/spells/registry_test.go +++ b/internal/modules/dnd/extract/spells/registry_test.go @@ -23,8 +23,9 @@ func TestNewRequiresLLMClientAndReturnsExtractor(t *testing.T) { func TestModuleSpec(t *testing.T) { got := ModuleSpec() want := pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageExtract, + Key: Key, + Stage: pipeline.StageExtract, + ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{ "chunks", "source.transcript", diff --git a/internal/modules/dnd/normalize/combatturns/normalizer.go b/internal/modules/dnd/normalize/combatturns/normalizer.go index 9c53c125..36c4bd70 100644 --- a/internal/modules/dnd/normalize/combatturns/normalizer.go +++ b/internal/modules/dnd/normalize/combatturns/normalizer.go @@ -336,6 +336,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageNormalize, + ExecutionClass: contracts.ExecutionClassDeterministic, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.CombatTurnListKind, diff --git a/internal/modules/dnd/normalize/itemevents/normalizer.go b/internal/modules/dnd/normalize/itemevents/normalizer.go index 930d324a..0524cbe1 100644 --- a/internal/modules/dnd/normalize/itemevents/normalizer.go +++ b/internal/modules/dnd/normalize/itemevents/normalizer.go @@ -222,11 +222,12 @@ func eventScope(index int) string { return fmt.Sprintf("events[%d]", index) } func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageNormalize, - Requires: append([]string(nil), requiredCapabilities...), - Provides: append([]string(nil), providedCapabilities...), - ArtifactKind: dnd.ItemEventListKind, + Key: Key, + Stage: pipeline.StageNormalize, + ExecutionClass: contracts.ExecutionClassDeterministic, + Requires: append([]string(nil), requiredCapabilities...), + Provides: append([]string(nil), providedCapabilities...), + ArtifactKind: dnd.ItemEventListKind, } } diff --git a/internal/modules/dnd/normalize/npcinteractions/normalizer.go b/internal/modules/dnd/normalize/npcinteractions/normalizer.go index e0f00f0c..df2d8213 100644 --- a/internal/modules/dnd/normalize/npcinteractions/normalizer.go +++ b/internal/modules/dnd/normalize/npcinteractions/normalizer.go @@ -278,6 +278,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageNormalize, + ExecutionClass: contracts.ExecutionClassDeterministic, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCInteractionListKind, diff --git a/internal/modules/dnd/normalize/npcs/normalizer.go b/internal/modules/dnd/normalize/npcs/normalizer.go index 65fe5650..cb2f269b 100644 --- a/internal/modules/dnd/normalize/npcs/normalizer.go +++ b/internal/modules/dnd/normalize/npcs/normalizer.go @@ -360,7 +360,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning { func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) } func ModuleSpec() pipeline.ModuleSpec { - return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCListKind} + return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCListKind} } func Register(registry *pipeline.NormalizerRegistry) error { diff --git a/internal/modules/dnd/normalize/npcs/normalizer_test.go b/internal/modules/dnd/normalize/npcs/normalizer_test.go index d4a5c790..7d57b7ca 100644 --- a/internal/modules/dnd/normalize/npcs/normalizer_test.go +++ b/internal/modules/dnd/normalize/npcs/normalizer_test.go @@ -21,7 +21,7 @@ func TestModuleContractAndIdentity(t *testing.T) { if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil { t.Fatal("DecodeOptions() accepted unknown option") } - want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.NPCListKind} + want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.NPCListKind} if got := ModuleSpec(); !reflect.DeepEqual(got, want) { t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) } diff --git a/internal/modules/dnd/normalize/scenedescriptions/normalizer.go b/internal/modules/dnd/normalize/scenedescriptions/normalizer.go index aa292c3d..1891f71b 100644 --- a/internal/modules/dnd/normalize/scenedescriptions/normalizer.go +++ b/internal/modules/dnd/normalize/scenedescriptions/normalizer.go @@ -138,11 +138,12 @@ func sourceRefLabel(ref source.SourceRef) string { func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageNormalize, - Requires: append([]string(nil), requiredCapabilities...), - Provides: append([]string(nil), providedCapabilities...), - ArtifactKind: dnd.SceneDescriptionListKind, + Key: Key, + Stage: pipeline.StageNormalize, + ExecutionClass: contracts.ExecutionClassDeterministic, + Requires: append([]string(nil), requiredCapabilities...), + Provides: append([]string(nil), providedCapabilities...), + ArtifactKind: dnd.SceneDescriptionListKind, } } diff --git a/internal/modules/dnd/normalize/scenedescriptions/normalizer_test.go b/internal/modules/dnd/normalize/scenedescriptions/normalizer_test.go index dd529d7e..7cf9436b 100644 --- a/internal/modules/dnd/normalize/scenedescriptions/normalizer_test.go +++ b/internal/modules/dnd/normalize/scenedescriptions/normalizer_test.go @@ -90,7 +90,7 @@ func TestNormalizerContractAndCancellation(t *testing.T) { if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil { t.Fatal("DecodeOptions() accepted unknown options") } - want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.SceneDescriptionListKind} + want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.SceneDescriptionListKind} if got := ModuleSpec(); !reflect.DeepEqual(got, want) { t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) } diff --git a/internal/modules/dnd/normalize/spells/normalizer.go b/internal/modules/dnd/normalize/spells/normalizer.go index 62dfdb98..ddcd2cde 100644 --- a/internal/modules/dnd/normalize/spells/normalizer.go +++ b/internal/modules/dnd/normalize/spells/normalizer.go @@ -297,6 +297,7 @@ func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageNormalize, + ExecutionClass: contracts.ExecutionClassDeterministic, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.SpellListKind, diff --git a/internal/modules/dnd/normalize/spells/normalizer_test.go b/internal/modules/dnd/normalize/spells/normalizer_test.go index 709e210b..48ebf3e9 100644 --- a/internal/modules/dnd/normalize/spells/normalizer_test.go +++ b/internal/modules/dnd/normalize/spells/normalizer_test.go @@ -24,11 +24,12 @@ func TestModuleContractAndStrictOptions(t *testing.T) { } want := pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageNormalize, - Requires: []string{"merged"}, - Provides: []string{"normalized"}, - ArtifactKind: dnd.SpellListKind, + Key: Key, + Stage: pipeline.StageNormalize, + ExecutionClass: contracts.ExecutionClassDeterministic, + Requires: []string{"merged"}, + Provides: []string{"normalized"}, + ArtifactKind: dnd.SpellListKind, ReferenceSlots: []contracts.ReferenceSlot{{ Name: spellcatalog.SpellCatalogReferenceSlot, Description: "Optional canonical spell-name catalog used for normalization and duplicate identity.", diff --git a/internal/modules/generic/chunk/units/chunker.go b/internal/modules/generic/chunk/units/chunker.go index d908703e..67e97575 100644 --- a/internal/modules/generic/chunk/units/chunker.go +++ b/internal/modules/generic/chunk/units/chunker.go @@ -90,9 +90,10 @@ func (c *Chunker) Plan(ctx context.Context, req contracts.ChunkRequest) (contrac func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageChunk, - Provides: []string{"chunks"}, + Key: Key, + Stage: pipeline.StageChunk, + ExecutionClass: contracts.ExecutionClassDeterministic, + Provides: []string{"chunks"}, } } diff --git a/internal/modules/generic/chunk/units/chunker_test.go b/internal/modules/generic/chunk/units/chunker_test.go index a3d963f6..73d35e81 100644 --- a/internal/modules/generic/chunk/units/chunker_test.go +++ b/internal/modules/generic/chunk/units/chunker_test.go @@ -15,9 +15,10 @@ import ( func TestModuleSpecAndRegister(t *testing.T) { want := pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageChunk, - Provides: []string{"chunks"}, + Key: Key, + Stage: pipeline.StageChunk, + ExecutionClass: contracts.ExecutionClassDeterministic, + Provides: []string{"chunks"}, } if got := ModuleSpec(); !reflect.DeepEqual(got, want) { t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) diff --git a/internal/modules/generic/merge/appendorder/merger.go b/internal/modules/generic/merge/appendorder/merger.go index 729d4f31..2a8d6009 100644 --- a/internal/modules/generic/merge/appendorder/merger.go +++ b/internal/modules/generic/merge/appendorder/merger.go @@ -3,13 +3,14 @@ package appendorder import ( "fmt" + "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) const Key = "appendorder" func ModuleSpec() pipeline.ModuleSpec { - return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageMerge, Provides: []string{"merged"}} + return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageMerge, ExecutionClass: contracts.ExecutionClassDeterministic, Provides: []string{"merged"}} } func mergerErrorf(format string, args ...any) error { diff --git a/internal/modules/generic/normalize/noop/normalizer.go b/internal/modules/generic/normalize/noop/normalizer.go index ebe416d7..dd05adfa 100644 --- a/internal/modules/generic/normalize/noop/normalizer.go +++ b/internal/modules/generic/normalize/noop/normalizer.go @@ -3,13 +3,14 @@ package noop import ( "fmt" + "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) const Key = "noop" func ModuleSpec() pipeline.ModuleSpec { - return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, Requires: []string{"merged"}, Provides: []string{"normalized"}} + return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"merged"}, Provides: []string{"normalized"}} } func normalizerErrorf(format string, args ...any) error { diff --git a/internal/modules/generic/output/json/encoder.go b/internal/modules/generic/output/json/encoder.go index 85067149..79982e67 100644 --- a/internal/modules/generic/output/json/encoder.go +++ b/internal/modules/generic/output/json/encoder.go @@ -80,10 +80,11 @@ func (e *Encoder) Encode(ctx context.Context, req contracts.OutputRequest) (cont func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageOutput, - Requires: []string{"normalized"}, - Provides: []string{"encoded"}, + Key: Key, + Stage: pipeline.StageOutput, + ExecutionClass: contracts.ExecutionClassDeterministic, + Requires: []string{"normalized"}, + Provides: []string{"encoded"}, } } diff --git a/internal/modules/generic/output/json/encoder_test.go b/internal/modules/generic/output/json/encoder_test.go index ad8ee554..b51551af 100644 --- a/internal/modules/generic/output/json/encoder_test.go +++ b/internal/modules/generic/output/json/encoder_test.go @@ -17,10 +17,11 @@ import ( func TestModuleSpecAndRegister(t *testing.T) { want := pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageOutput, - Requires: []string{"normalized"}, - Provides: []string{"encoded"}, + Key: Key, + Stage: pipeline.StageOutput, + ExecutionClass: contracts.ExecutionClassDeterministic, + Requires: []string{"normalized"}, + Provides: []string{"encoded"}, } if got := ModuleSpec(); !reflect.DeepEqual(got, want) { t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) diff --git a/internal/modules/seriatim/input/transcript/adapter.go b/internal/modules/seriatim/input/transcript/adapter.go index 7411b5f4..336cc51f 100644 --- a/internal/modules/seriatim/input/transcript/adapter.go +++ b/internal/modules/seriatim/input/transcript/adapter.go @@ -94,9 +94,10 @@ func (a *Adapter) Parse(ctx context.Context, req contracts.ParseRequest) (*sourc func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageInput, - Provides: append([]string(nil), providedCapabilities...), + Key: Key, + Stage: pipeline.StageInput, + ExecutionClass: contracts.ExecutionClassDeterministic, + Provides: append([]string(nil), providedCapabilities...), } } diff --git a/internal/modules/seriatim/input/transcript/registry_test.go b/internal/modules/seriatim/input/transcript/registry_test.go index e76ecc45..e135f635 100644 --- a/internal/modules/seriatim/input/transcript/registry_test.go +++ b/internal/modules/seriatim/input/transcript/registry_test.go @@ -7,6 +7,7 @@ import ( "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/source" + "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) @@ -23,8 +24,9 @@ func TestNewReturnsAdapterWithKey(t *testing.T) { func TestModuleSpec(t *testing.T) { got := ModuleSpec() want := pipeline.ModuleSpec{ - Key: Key, - Stage: pipeline.StageInput, + Key: Key, + Stage: pipeline.StageInput, + ExecutionClass: contracts.ExecutionClassDeterministic, Provides: []string{ "source.transcript", "transcript.speaker",