Add execution metadata to module specifications

This commit is contained in:
2026-08-03 16:49:51 +00:00
parent a3bd0c1867
commit ce857966f1
39 changed files with 331 additions and 123 deletions

View File

@@ -24,7 +24,11 @@ does not repeat their JSON shapes or schemas.
The D&D registrar registers the familys artifact codecs, extractors, typed The D&D registrar registers the familys artifact codecs, extractors, typed
append-order mergers, normalizers, validators, prompt assets, fallback LLM append-order mergers, normalizers, validators, prompt assets, fallback LLM
profile asset, and default validator chains. Each extractor and normalizer has 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 Configuration remains the canonical owner of the exact keys and validator
order. order.

View File

@@ -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 A module is a typed implementation registered for one pipeline stage. Its
`ModuleSpec` is the public-to-the-framework declaration of its stable key, `ModuleSpec` is the public-to-the-framework declaration of its stable key,
stage, required and provided capabilities, artifact kind, and accepted stage, execution class, required and provided capabilities, artifact kind, and
reference slots. The framework uses that declaration to resolve a configured accepted reference slots. The execution class states whether a module is
binding before it builds the implementation. `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 Implementations that accept options must provide both an option validator and
a builder. The validator is used while resolving configuration; the builder 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 1. Choose the pipeline stage and the typed artifact boundary. Put external
input or durable artifact formats in the relevant integration contract, input or durable artifact formats in the relevant integration contract,
not in this guide or in a private LLM response type. not in this guide or in a private LLM response type.
2. Define a stable `ModuleSpec` with the exact capabilities and reference 2. Define a stable `ModuleSpec` with an explicit execution class, the exact
slots needed for the operation. Model a producer/consumer handoff as an capabilities, and reference slots needed for the operation. Model a
artifact-compatible slot; configuration then chooses an external file or a producer/consumer handoff as an artifact-compatible slot; configuration
generated binding. then chooses an external file or a generated binding.
3. Implement strict option decoding, construction, and the typed stage 3. Implement strict option decoding, construction, and the typed stage
interface. Preserve caller ownership: do not retain mutable request data interface. Preserve caller ownership: do not retain mutable request data
and return defensive copies where an implementation exposes stored data. and return defensive copies where an implementation exposes stored data.

View File

@@ -166,6 +166,35 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
assertProductionContains(t, "production prompt assets", assetNames, requiredAssets) assertProductionContains(t, "production prompt assets", assetNames, requiredAssets)
catalog := catalogFromRegistries(registries) 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) converted := registriesFromCatalog(catalog)
if converted.ArtifactCodecs != registries.ArtifactCodecs || converted.ArtifactEvidence != registries.ArtifactEvidence || converted.ValidatorChains != registries.ValidatorChains { 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") t.Fatal("catalog/registry conversion did not preserve artifact and validator registries")

View File

@@ -19,23 +19,28 @@ func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) {
} }
resolved := pipeline.ResolvedPipeline{ resolved := pipeline.ResolvedPipeline{
ID: "redaction-test", ID: "redaction-test",
Digest: "sha256:safe-digest", Digest: "sha256:safe-digest",
Input: bindings["input"], Input: bindings["input"],
Chunk: bindings["chunk"], InputExecutionClass: contracts.ExecutionClassDeterministic,
ChunkReferences: redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"), Chunk: bindings["chunk"],
ChunkExecutionClass: contracts.ExecutionClassLLMBacked,
ChunkReferences: redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"),
Steps: []pipeline.ResolvedPipelineStep{{ Steps: []pipeline.ResolvedPipelineStep{{
ID: "default", ID: "default",
ArtifactLanes: []pipeline.ResolvedArtifactLane{{ ArtifactLanes: []pipeline.ResolvedArtifactLane{{
ID: "safe-lane", ID: "safe-lane",
ArtifactKind: "safe/artifact", ArtifactKind: "safe/artifact",
Extract: bindings["extract"], Extract: bindings["extract"],
Merge: bindings["merge"], ExtractExecutionClass: contracts.ExecutionClassLLMBacked,
Normalize: bindings["normalize"], Merge: bindings["merge"],
Validators: []pipeline.ModuleBinding{bindings["lane-validator"]}, MergeExecutionClass: contracts.ExecutionClassDeterministic,
ExtractReferences: redactionTestReferenceTarget(pipeline.StageExtract, "safe-lane", "extract-reference-content"), Normalize: bindings["normalize"],
MergeReferences: redactionTestReferenceTarget(pipeline.StageMerge, "safe-lane", "merge-reference-content"), NormalizeExecutionClass: contracts.ExecutionClassLLMBacked,
NormalizeReferences: redactionTestReferenceTarget(pipeline.StageNormalize, "safe-lane", "normalize-reference-content"), 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{{ ValidatorChains: []pipeline.ResolvedValidatorChain{{
@@ -49,7 +54,8 @@ func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) {
ArtifactKind: "safe/artifact", ArtifactKind: "safe/artifact",
}}, }},
}}, }},
Output: bindings["output"], Output: bindings["output"],
OutputExecutionClass: contracts.ExecutionClassDeterministic,
} }
effective := EffectiveConfig{ effective := EffectiveConfig{
Config: Config{Pipelines: map[string]pipeline.PipelineProfile{ 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) 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" payload.Input.Options["safe"] = "mutated"
nested := payload.Input.Options["nested"].([]any)[0].([]any)[0].(map[string]any) nested := payload.Input.Options["nested"].([]any)[0].([]any)[0].(map[string]any)

View File

@@ -115,10 +115,11 @@ func runRegistryBehaviorTests[M any](t *testing.T, testCase registryBehaviorCase
t.Fatal("Spec() ok = false, want true") t.Fatal("Spec() ok = false, want true")
} }
want := ModuleSpec{ want := ModuleSpec{
Key: testCase.key, Key: testCase.key,
Stage: testCase.stage, Stage: testCase.stage,
Provides: []string{"alpha", "beta"}, ExecutionClass: contracts.ExecutionClassDeterministic,
Requires: []string{"source"}, Provides: []string{"alpha", "beta"},
Requires: []string{"source"},
} }
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
t.Fatalf("Spec() = %#v, want %#v", 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 { if !ok {
t.Fatal("Spec() ok = false, want true") 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) { if !reflect.DeepEqual(spec, want) {
t.Fatalf("Spec() = %#v, want %#v", spec, want) t.Fatalf("Spec() = %#v, want %#v", spec, want)
} }

View File

@@ -61,10 +61,11 @@ func TestInputAdapterRegistryRegisterWithSpecStoresMetadata(t *testing.T) {
t.Fatal("Spec() ok = false, want true") t.Fatal("Spec() ok = false, want true")
} }
want := ModuleSpec{ want := ModuleSpec{
Key: "generic-input", Key: "generic-input",
Stage: StageInput, Stage: StageInput,
Provides: []string{"parsed-source", "source-document"}, ExecutionClass: contracts.ExecutionClassDeterministic,
Requires: []string{"raw-bytes"}, Provides: []string{"parsed-source", "source-document"},
Requires: []string{"raw-bytes"},
} }
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
t.Fatalf("Spec() = %#v, want %#v", got, want) t.Fatalf("Spec() = %#v, want %#v", got, want)
@@ -91,7 +92,7 @@ func TestInputAdapterRegistryRegisterStoresDefaultSpec(t *testing.T) {
if !ok { if !ok {
t.Fatal("Spec() ok = false, want true") 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) { if !reflect.DeepEqual(got, want) {
t.Fatalf("Spec() = %#v, want %#v", got, want) t.Fatalf("Spec() = %#v, want %#v", got, want)
} }

View File

@@ -23,6 +23,7 @@ const (
type ModuleSpec struct { type ModuleSpec struct {
Key string Key string
Stage ModuleStage Stage ModuleStage
ExecutionClass contracts.ExecutionClass
ArtifactKind contracts.ArtifactKind ArtifactKind contracts.ArtifactKind
Provides []string Provides []string
Requires []string Requires []string
@@ -37,9 +38,14 @@ func defaultModuleSpec(key string, stage ModuleStage) ModuleSpec {
} }
func normalizeModuleSpec(spec ModuleSpec) ModuleSpec { func normalizeModuleSpec(spec ModuleSpec) ModuleSpec {
executionClass := contracts.ExecutionClass(strings.TrimSpace(string(spec.ExecutionClass)))
if executionClass == "" {
executionClass = contracts.ExecutionClassDeterministic
}
return ModuleSpec{ return ModuleSpec{
Key: strings.TrimSpace(spec.Key), Key: strings.TrimSpace(spec.Key),
Stage: spec.Stage, Stage: spec.Stage,
ExecutionClass: executionClass,
ArtifactKind: normalizeArtifactKind(spec.ArtifactKind), ArtifactKind: normalizeArtifactKind(spec.ArtifactKind),
Provides: normalizeCapabilities(spec.Provides), Provides: normalizeCapabilities(spec.Provides),
Requires: normalizeCapabilities(spec.Requires), Requires: normalizeCapabilities(spec.Requires),
@@ -76,6 +82,7 @@ func cloneModuleSpec(spec ModuleSpec) ModuleSpec {
return ModuleSpec{ return ModuleSpec{
Key: spec.Key, Key: spec.Key,
Stage: spec.Stage, Stage: spec.Stage,
ExecutionClass: spec.ExecutionClass,
ArtifactKind: spec.ArtifactKind, ArtifactKind: spec.ArtifactKind,
Provides: append([]string(nil), spec.Provides...), Provides: append([]string(nil), spec.Provides...),
Requires: append([]string(nil), spec.Requires...), Requires: append([]string(nil), spec.Requires...),

View File

@@ -1,12 +1,25 @@
package pipeline package pipeline
import ( import (
"reflect"
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "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) { func TestValidateModuleSpecAllowsReferenceSlotsForEligibleStages(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@@ -156,20 +156,23 @@ type ResolvedReferenceTarget struct {
} }
type ResolvedArtifactLane struct { type ResolvedArtifactLane struct {
StepID string StepID string
ID string ID string
ArtifactKind contracts.ArtifactKind `json:"artifact_kind,omitempty"` ArtifactKind contracts.ArtifactKind `json:"artifact_kind,omitempty"`
ArtifactSchemaID string `json:"artifact_schema_id,omitempty"` ArtifactSchemaID string `json:"artifact_schema_id,omitempty"`
ArtifactSchemaName string `json:"artifact_schema_name,omitempty"` ArtifactSchemaName string `json:"artifact_schema_name,omitempty"`
ArtifactSchemaVersion string `json:"artifact_schema_version,omitempty"` ArtifactSchemaVersion string `json:"artifact_schema_version,omitempty"`
ArtifactSchemaDigest string `json:"artifact_schema_digest,omitempty"` ArtifactSchemaDigest string `json:"artifact_schema_digest,omitempty"`
Extract ModuleBinding Extract ModuleBinding
Merge ModuleBinding ExtractExecutionClass contracts.ExecutionClass `json:"extract_execution_class"`
Normalize ModuleBinding Merge ModuleBinding
Validators []ModuleBinding MergeExecutionClass contracts.ExecutionClass `json:"merge_execution_class"`
ExtractReferences ResolvedReferenceTarget `json:"extract_references"` Normalize ModuleBinding
MergeReferences ResolvedReferenceTarget `json:"merge_references"` NormalizeExecutionClass contracts.ExecutionClass `json:"normalize_execution_class"`
NormalizeReferences ResolvedReferenceTarget `json:"normalize_references"` Validators []ModuleBinding
ExtractReferences ResolvedReferenceTarget `json:"extract_references"`
MergeReferences ResolvedReferenceTarget `json:"merge_references"`
NormalizeReferences ResolvedReferenceTarget `json:"normalize_references"`
} }
type ResolvedPipelineStep struct { type ResolvedPipelineStep struct {
@@ -192,14 +195,17 @@ type ResolvedValidator struct {
} }
type ResolvedPipeline struct { type ResolvedPipeline struct {
ID string ID string
Digest string Digest string
Input ModuleBinding Input ModuleBinding
Chunk ModuleBinding InputExecutionClass contracts.ExecutionClass `json:"input_execution_class"`
ChunkReferences ResolvedReferenceTarget `json:"chunk_references"` Chunk ModuleBinding
Steps []ResolvedPipelineStep ChunkExecutionClass contracts.ExecutionClass `json:"chunk_execution_class"`
ValidatorChains []ResolvedValidatorChain `json:"validator_chains"` ChunkReferences ResolvedReferenceTarget `json:"chunk_references"`
Output ModuleBinding 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 // AllArtifactLanes returns lanes in deterministic step order for read-only
@@ -225,6 +231,46 @@ type ModuleCatalog struct {
Outputs *OutputEncoderRegistry 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 { func Binding(module string) ModuleBinding {
return ModuleBinding{Module: strings.TrimSpace(module)} return ModuleBinding{Module: strings.TrimSpace(module)}
} }
@@ -316,11 +362,13 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
return ResolvedPipeline{}, err return ResolvedPipeline{}, err
} }
resolved := ResolvedPipeline{ resolved := ResolvedPipeline{
ID: pipelineID, ID: pipelineID,
Input: input, Input: input,
Chunk: chunk, InputExecutionClass: inputModuleSpec.ExecutionClass,
ChunkReferences: referenceTarget(StageChunk, "", chunk.Module, chunkReferences), Chunk: chunk,
Output: resolveBinding(profile.Output, DefaultOutputModule), 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) chunkValidatorChain, err := resolveValidatorChain(pipelineID, "", StageChunk, chunk.Module, chunk.Validators, "", nil, catalog)
if err != nil { if err != nil {
@@ -386,6 +434,7 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
if missing, ok := outputCapabilities.missing(outputSpec.Requires); ok { if missing, ok := outputCapabilities.missing(outputSpec.Requires); ok {
return ResolvedPipeline{}, capabilityError(pipelineID, "", StageOutput, resolved.Output.Module, missing) return ResolvedPipeline{}, capabilityError(pipelineID, "", StageOutput, resolved.Output.Module, missing)
} }
resolved.OutputExecutionClass = outputSpec.ExecutionClass
if err := validateResolvedOptions(resolved, catalog, configuredLaneIDs); err != nil { if err := validateResolvedOptions(resolved, catalog, configuredLaneIDs); err != nil {
return ResolvedPipeline{}, err return ResolvedPipeline{}, err
} }
@@ -475,6 +524,7 @@ func resolveArtifactLane(
} }
lane.ExtractReferences = referenceTarget(StageExtract, laneID, lane.Extract.Module, references) lane.ExtractReferences = referenceTarget(StageExtract, laneID, lane.Extract.Module, references)
lane.ExtractReferences.StepID = strings.TrimSpace(stepID) lane.ExtractReferences.StepID = strings.TrimSpace(stepID)
lane.ExtractExecutionClass = extractSpec.ExecutionClass
capabilities.add(extractSpec.Provides...) capabilities.add(extractSpec.Provides...)
mergeSpec, err := mergerSpecForArtifact(catalog, lane.Merge.Module, lane.ArtifactKind, artifactType) 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 = referenceTarget(StageMerge, laneID, lane.Merge.Module, mergeReferences)
lane.MergeReferences.StepID = strings.TrimSpace(stepID) lane.MergeReferences.StepID = strings.TrimSpace(stepID)
lane.MergeExecutionClass = mergeSpec.ExecutionClass
capabilities.add(mergeSpec.Provides...) capabilities.add(mergeSpec.Provides...)
normalizeSpec, err := normalizerSpecForArtifact(catalog, lane.Normalize.Module, lane.ArtifactKind, artifactType) 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 = referenceTarget(StageNormalize, laneID, lane.Normalize.Module, normalizeReferences)
lane.NormalizeReferences.StepID = strings.TrimSpace(stepID) lane.NormalizeReferences.StepID = strings.TrimSpace(stepID)
lane.NormalizeExecutionClass = normalizeSpec.ExecutionClass
capabilities.add(normalizeSpec.Provides...) capabilities.add(normalizeSpec.Provides...)
if len(lane.Validators) > 0 { if len(lane.Validators) > 0 {
@@ -1307,21 +1359,27 @@ func selectedArtifactLanes(pipelineID string, artifacts map[string]ArtifactLaneP
func resolvedPipelineDigest(resolved ResolvedPipeline) (string, error) { func resolvedPipelineDigest(resolved ResolvedPipeline) (string, error) {
withoutDigest := struct { withoutDigest := struct {
ID string ID string
Input ModuleBinding Input ModuleBinding
Chunk ModuleBinding InputExecutionClass contracts.ExecutionClass
ChunkReferences ResolvedReferenceTarget Chunk ModuleBinding
Steps []ResolvedPipelineStep ChunkExecutionClass contracts.ExecutionClass
ValidatorChains []ResolvedValidatorChain ChunkReferences ResolvedReferenceTarget
Output ModuleBinding Steps []ResolvedPipelineStep
ValidatorChains []ResolvedValidatorChain
Output ModuleBinding
OutputExecutionClass contracts.ExecutionClass
}{ }{
ID: resolved.ID, ID: resolved.ID,
Input: resolved.Input, Input: resolved.Input,
Chunk: resolved.Chunk, InputExecutionClass: resolved.InputExecutionClass,
ChunkReferences: resolved.ChunkReferences, Chunk: resolved.Chunk,
Steps: resolved.Steps, ChunkExecutionClass: resolved.ChunkExecutionClass,
ValidatorChains: resolved.ValidatorChains, ChunkReferences: resolved.ChunkReferences,
Output: resolved.Output, Steps: resolved.Steps,
ValidatorChains: resolved.ValidatorChains,
Output: resolved.Output,
OutputExecutionClass: resolved.OutputExecutionClass,
} }
encoded, err := json.Marshal(withoutDigest) encoded, err := json.Marshal(withoutDigest)
if err != nil { if err != nil {

View File

@@ -47,12 +47,18 @@ func TestResolvePipelineWithExplicitModules(t *testing.T) {
if !reflect.DeepEqual(resolved.Input, ModuleBinding{Module: "text", LLMProfile: "fast"}) { if !reflect.DeepEqual(resolved.Input, ModuleBinding{Module: "text", LLMProfile: "fast"}) {
t.Fatalf("Input = %#v, want trimmed explicit input", resolved.Input) 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 != "" { if resolved.Chunk.Module != "window" || resolved.Chunk.LLMProfile != "" {
t.Fatalf("Chunk = %#v, want explicit module and empty LLM profile", resolved.Chunk) t.Fatalf("Chunk = %#v, want explicit module and empty LLM profile", resolved.Chunk)
} }
if resolved.Chunk.Options["size"] != 10 { if resolved.Chunk.Options["size"] != 10 {
t.Fatalf("Chunk.Options = %#v, want size option", resolved.Chunk.Options) 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 { 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) 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"}) { if !reflect.DeepEqual(lane.Extract, ModuleBinding{Module: "record-extractor", LLMProfile: "careful"}) {
t.Fatalf("lane.Extract = %#v, want explicit extractor", lane.Extract) 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" { if lane.Merge.Module != "dedupe" || lane.Normalize.Module != "canonical" {
t.Fatalf("lane merge/normalize = %#v/%#v, want explicit modules", lane.Merge, lane.Normalize) 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" { if resolved.Output.Module != "ndjson" {
t.Fatalf("Output.Module = %q, want ndjson", resolved.Output.Module) 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:") { if !strings.HasPrefix(resolved.Digest, "sha256:") {
t.Fatalf("Digest = %q, want sha256 digest", resolved.Digest) 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) { func TestResolvePipelineAppliesDefaults(t *testing.T) {
resolved, err := ResolvePipeline(PipelineProfile{ resolved, err := ResolvePipeline(PipelineProfile{
ID: "defaulted", ID: "defaulted",
@@ -1566,7 +1615,13 @@ func TestResolvedPipelineCanMarshalToCanonicalJSON(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("ResolvePipeline() error = %v, want nil", err) 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) 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)
}
}
} }

View File

@@ -120,6 +120,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageChunk, Stage: pipeline.StageChunk,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions), ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions),

View File

@@ -24,6 +24,7 @@ func TestNewModuleSpecAndRegister(t *testing.T) {
want := pipeline.ModuleSpec{ want := pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageChunk, Stage: pipeline.StageChunk,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: []string{"source.transcript"}, Requires: []string{"source.transcript"},
Provides: []string{"chunks"}, Provides: []string{"chunks"},
ReferenceSlots: wantReferenceSlots(), ReferenceSlots: wantReferenceSlots(),

View File

@@ -226,6 +226,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.CombatTurnListKind, ArtifactKind: dnd.CombatTurnListKind,

View File

@@ -129,6 +129,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.ItemEventListKind, ArtifactKind: dnd.ItemEventListKind,

View File

@@ -18,7 +18,7 @@ func TestConstructorSpecOptionsAndMetadata(t *testing.T) {
t.Fatalf("New() error = %v", err) t.Fatalf("New() error = %v", err)
} }
want := pipeline.ModuleSpec{ 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{ ReferenceSlots: []contracts.ReferenceSlot{
{Name: "glossary", Description: referenceSlotDescriptions.Glossary, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}}, {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"}}, {Name: "party", Description: referenceSlotDescriptions.Party, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},

View File

@@ -177,6 +177,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.NPCInteractionListKind, ArtifactKind: dnd.NPCInteractionListKind,

View File

@@ -135,6 +135,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.NPCListKind, ArtifactKind: dnd.NPCListKind,

View File

@@ -25,11 +25,12 @@ func TestNewRequiresLLMClientAndRejectsAmbiguousReferences(t *testing.T) {
func TestModuleSpecAndReferenceSlots(t *testing.T) { func TestModuleSpecAndReferenceSlots(t *testing.T) {
got := ModuleSpec() got := ModuleSpec()
want := pipeline.ModuleSpec{ want := pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
Requires: []string{"chunks", "source.transcript"}, ExecutionClass: contracts.ExecutionClassLLMBacked,
Provides: []string{"dnd.npcs"}, Requires: []string{"chunks", "source.transcript"},
ArtifactKind: dnd.NPCListKind, Provides: []string{"dnd.npcs"},
ArtifactKind: dnd.NPCListKind,
ReferenceSlots: []contracts.ReferenceSlot{ 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: "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"}}, {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"}},

View File

@@ -146,6 +146,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.SceneDescriptionListKind, ArtifactKind: dnd.SceneDescriptionListKind,

View File

@@ -24,7 +24,7 @@ func TestNewRequiresLLMClientAndRejectsAmbiguousReferences(t *testing.T) {
func TestModuleSpecAndReferenceSlots(t *testing.T) { func TestModuleSpecAndReferenceSlots(t *testing.T) {
want := pipeline.ModuleSpec{ 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{ 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: "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"}}, {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"}},

View File

@@ -200,6 +200,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.SpellListKind, ArtifactKind: dnd.SpellListKind,

View File

@@ -23,8 +23,9 @@ func TestNewRequiresLLMClientAndReturnsExtractor(t *testing.T) {
func TestModuleSpec(t *testing.T) { func TestModuleSpec(t *testing.T) {
got := ModuleSpec() got := ModuleSpec()
want := pipeline.ModuleSpec{ want := pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageExtract, Stage: pipeline.StageExtract,
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: []string{ Requires: []string{
"chunks", "chunks",
"source.transcript", "source.transcript",

View File

@@ -336,6 +336,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageNormalize, Stage: pipeline.StageNormalize,
ExecutionClass: contracts.ExecutionClassDeterministic,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.CombatTurnListKind, ArtifactKind: dnd.CombatTurnListKind,

View File

@@ -222,11 +222,12 @@ func eventScope(index int) string { return fmt.Sprintf("events[%d]", index) }
func ModuleSpec() pipeline.ModuleSpec { func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageNormalize, Stage: pipeline.StageNormalize,
Requires: append([]string(nil), requiredCapabilities...), ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: append([]string(nil), providedCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
ArtifactKind: dnd.ItemEventListKind, Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.ItemEventListKind,
} }
} }

View File

@@ -278,6 +278,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageNormalize, Stage: pipeline.StageNormalize,
ExecutionClass: contracts.ExecutionClassDeterministic,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.NPCInteractionListKind, ArtifactKind: dnd.NPCInteractionListKind,

View File

@@ -360,7 +360,7 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) } func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) }
func ModuleSpec() pipeline.ModuleSpec { 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 { func Register(registry *pipeline.NormalizerRegistry) error {

View File

@@ -21,7 +21,7 @@ func TestModuleContractAndIdentity(t *testing.T) {
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil { if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
t.Fatal("DecodeOptions() accepted unknown option") 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) { if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
} }

View File

@@ -138,11 +138,12 @@ func sourceRefLabel(ref source.SourceRef) string {
func ModuleSpec() pipeline.ModuleSpec { func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageNormalize, Stage: pipeline.StageNormalize,
Requires: append([]string(nil), requiredCapabilities...), ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: append([]string(nil), providedCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
ArtifactKind: dnd.SceneDescriptionListKind, Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.SceneDescriptionListKind,
} }
} }

View File

@@ -90,7 +90,7 @@ func TestNormalizerContractAndCancellation(t *testing.T) {
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil { if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
t.Fatal("DecodeOptions() accepted unknown options") 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) { if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
} }

View File

@@ -297,6 +297,7 @@ func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageNormalize, Stage: pipeline.StageNormalize,
ExecutionClass: contracts.ExecutionClassDeterministic,
Requires: append([]string(nil), requiredCapabilities...), Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.SpellListKind, ArtifactKind: dnd.SpellListKind,

View File

@@ -24,11 +24,12 @@ func TestModuleContractAndStrictOptions(t *testing.T) {
} }
want := pipeline.ModuleSpec{ want := pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageNormalize, Stage: pipeline.StageNormalize,
Requires: []string{"merged"}, ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: []string{"normalized"}, Requires: []string{"merged"},
ArtifactKind: dnd.SpellListKind, Provides: []string{"normalized"},
ArtifactKind: dnd.SpellListKind,
ReferenceSlots: []contracts.ReferenceSlot{{ ReferenceSlots: []contracts.ReferenceSlot{{
Name: spellcatalog.SpellCatalogReferenceSlot, Name: spellcatalog.SpellCatalogReferenceSlot,
Description: "Optional canonical spell-name catalog used for normalization and duplicate identity.", Description: "Optional canonical spell-name catalog used for normalization and duplicate identity.",

View File

@@ -90,9 +90,10 @@ func (c *Chunker) Plan(ctx context.Context, req contracts.ChunkRequest) (contrac
func ModuleSpec() pipeline.ModuleSpec { func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageChunk, Stage: pipeline.StageChunk,
Provides: []string{"chunks"}, ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: []string{"chunks"},
} }
} }

View File

@@ -15,9 +15,10 @@ import (
func TestModuleSpecAndRegister(t *testing.T) { func TestModuleSpecAndRegister(t *testing.T) {
want := pipeline.ModuleSpec{ want := pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageChunk, Stage: pipeline.StageChunk,
Provides: []string{"chunks"}, ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: []string{"chunks"},
} }
if got := ModuleSpec(); !reflect.DeepEqual(got, want) { if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)

View File

@@ -3,13 +3,14 @@ package appendorder
import ( import (
"fmt" "fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
) )
const Key = "appendorder" const Key = "appendorder"
func ModuleSpec() pipeline.ModuleSpec { 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 { func mergerErrorf(format string, args ...any) error {

View File

@@ -3,13 +3,14 @@ package noop
import ( import (
"fmt" "fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
) )
const Key = "noop" const Key = "noop"
func ModuleSpec() pipeline.ModuleSpec { 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 { func normalizerErrorf(format string, args ...any) error {

View File

@@ -80,10 +80,11 @@ func (e *Encoder) Encode(ctx context.Context, req contracts.OutputRequest) (cont
func ModuleSpec() pipeline.ModuleSpec { func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageOutput, Stage: pipeline.StageOutput,
Requires: []string{"normalized"}, ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: []string{"encoded"}, Requires: []string{"normalized"},
Provides: []string{"encoded"},
} }
} }

View File

@@ -17,10 +17,11 @@ import (
func TestModuleSpecAndRegister(t *testing.T) { func TestModuleSpecAndRegister(t *testing.T) {
want := pipeline.ModuleSpec{ want := pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageOutput, Stage: pipeline.StageOutput,
Requires: []string{"normalized"}, ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: []string{"encoded"}, Requires: []string{"normalized"},
Provides: []string{"encoded"},
} }
if got := ModuleSpec(); !reflect.DeepEqual(got, want) { if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)

View File

@@ -94,9 +94,10 @@ func (a *Adapter) Parse(ctx context.Context, req contracts.ParseRequest) (*sourc
func ModuleSpec() pipeline.ModuleSpec { func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageInput, Stage: pipeline.StageInput,
Provides: append([]string(nil), providedCapabilities...), ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: append([]string(nil), providedCapabilities...),
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"testing" "testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
) )
@@ -23,8 +24,9 @@ func TestNewReturnsAdapterWithKey(t *testing.T) {
func TestModuleSpec(t *testing.T) { func TestModuleSpec(t *testing.T) {
got := ModuleSpec() got := ModuleSpec()
want := pipeline.ModuleSpec{ want := pipeline.ModuleSpec{
Key: Key, Key: Key,
Stage: pipeline.StageInput, Stage: pipeline.StageInput,
ExecutionClass: contracts.ExecutionClassDeterministic,
Provides: []string{ Provides: []string{
"source.transcript", "source.transcript",
"transcript.speaker", "transcript.speaker",