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 5c5c56b..62268b9 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 9586e88..3f70f21 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 04070fb..ec05075 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 4029406..1ad7565 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 4c6640e..56934c7 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 e995504..f03c09c 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 1bbb4e7..2d29a2e 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 acb6b3d..2b98f17 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 e8c788d..ea1bc2b 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 a10345b..1df59b2 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 61a71fe..7005d6f 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 1df9cba..24ab6b7 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 98e891a..f64483c 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 280c17a..03e42d1 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 640ecbf..5aed4e3 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 0a5091a..5c01564 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 886a11d..e6f02ea 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 9cab7f4..bd81431 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 9ce7001..6188fd1 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 547c599..d717c86 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 d609493..db5cdf0 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 e2271ba..2e944ae 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 9c53c12..36c4bd7 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 930d324..0524cbe 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 e0f00f0..df2d821 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 65fe565..cb2f269 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 d4a5c79..7d57b7c 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 aa292c3..1891f71 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 dd529d7..7cf9436 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 62dfdb9..ddcd2cd 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 709e210..48ebf3e 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 d908703..67e9757 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 a3d963f..73d35e8 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 729d4f3..2a8d600 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 ebe416d..dd05adf 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 8506714..79982e6 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 ad8ee55..b51551a 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 7411b5f..336cc51 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 e76ecc4..e135f63 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",