Reject ambiguous configuration and reference bindings
This commit is contained in:
@@ -147,7 +147,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
machineOutput := fs.Bool("json", false, "write the successful run result as JSON")
|
machineOutput := fs.Bool("json", false, "write the successful run result as JSON")
|
||||||
debug := fs.Bool("debug", false, "write a debug bundle")
|
debug := fs.Bool("debug", false, "write a debug bundle")
|
||||||
debugDir := fs.String("debug-dir", "", "debug bundle directory")
|
debugDir := fs.String("debug-dir", "", "debug bundle directory")
|
||||||
llmProfile := fs.String("llm-profile", "", "LLM profile override")
|
llmProfile := singleValueFlag{name: "--llm-profile"}
|
||||||
reasoningEffort := singleValueFlag{name: "--reasoning-effort"}
|
reasoningEffort := singleValueFlag{name: "--reasoning-effort"}
|
||||||
clearReasoningEffort := fs.Bool("clear-reasoning-effort", false, "clear the LLM profile reasoning effort")
|
clearReasoningEffort := fs.Bool("clear-reasoning-effort", false, "clear the LLM profile reasoning effort")
|
||||||
resume := fs.Bool("resume", false, "reuse compatible recorded checkpoints")
|
resume := fs.Bool("resume", false, "reuse compatible recorded checkpoints")
|
||||||
@@ -157,6 +157,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
referenceFlags := stringListFlag{}
|
referenceFlags := stringListFlag{}
|
||||||
withoutReferenceFlags := stringListFlag{}
|
withoutReferenceFlags := stringListFlag{}
|
||||||
fs.Var(&requestedSessionID, "session-id", "prompt session identifier")
|
fs.Var(&requestedSessionID, "session-id", "prompt session identifier")
|
||||||
|
fs.Var(&llmProfile, "llm-profile", "LLM profile override")
|
||||||
fs.Var(&reasoningEffort, "reasoning-effort", "reasoning effort override")
|
fs.Var(&reasoningEffort, "reasoning-effort", "reasoning effort override")
|
||||||
fs.Var(&chunkCache, "chunk_cache", "chunk plan cache mode: auto, bypass, or refresh")
|
fs.Var(&chunkCache, "chunk_cache", "chunk plan cache mode: auto, bypass, or refresh")
|
||||||
fs.Var(&referenceFlags, "reference", "reference binding, as slot=path, chunk.slot=path, merge.slot=path, lane.slot=path, lane.extract.slot=path, lane.merge.slot=path, or lane.normalize.slot=path")
|
fs.Var(&referenceFlags, "reference", "reference binding, as slot=path, chunk.slot=path, merge.slot=path, lane.slot=path, lane.extract.slot=path, lane.merge.slot=path, or lane.normalize.slot=path")
|
||||||
@@ -203,6 +204,10 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
fmt.Fprintln(stderr, "notarius: --session-id must not be empty")
|
fmt.Fprintln(stderr, "notarius: --session-id must not be empty")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
if llmProfile.set && strings.TrimSpace(llmProfile.value) == "" {
|
||||||
|
fmt.Fprintln(stderr, "notarius: --llm-profile must not be empty")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
if reasoningEffort.set && *clearReasoningEffort {
|
if reasoningEffort.set && *clearReasoningEffort {
|
||||||
fmt.Fprintln(stderr, "notarius: --reasoning-effort cannot be combined with --clear-reasoning-effort")
|
fmt.Fprintln(stderr, "notarius: --reasoning-effort cannot be combined with --clear-reasoning-effort")
|
||||||
return 2
|
return 2
|
||||||
@@ -338,7 +343,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
PipelineID: pipelineID,
|
PipelineID: pipelineID,
|
||||||
Only: only,
|
Only: only,
|
||||||
Catalog: catalog,
|
Catalog: catalog,
|
||||||
LLMProfileOverride: *llmProfile,
|
LLMProfileOverride: strings.TrimSpace(llmProfile.value),
|
||||||
ReferenceOverrides: referenceOverrides,
|
ReferenceOverrides: referenceOverrides,
|
||||||
ReferenceUnbinds: referenceUnbinds,
|
ReferenceUnbinds: referenceUnbinds,
|
||||||
})
|
})
|
||||||
@@ -426,7 +431,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||||
}
|
}
|
||||||
checkpointRecorder, checkpointLoader, err := checkpointHandlersForRun(effective.Config.Cache.Checkpoints, opts, effective.ResolvedPipeline, prepared.CheckpointFingerprints(), llmFingerprints, rawInput, only, llmProfiles, strings.TrimSpace(*llmProfile), effectiveSessionID, runtimeOverrides, *resume)
|
checkpointRecorder, checkpointLoader, err := checkpointHandlersForRun(effective.Config.Cache.Checkpoints, opts, effective.ResolvedPipeline, prepared.CheckpointFingerprints(), llmFingerprints, rawInput, only, llmProfiles, strings.TrimSpace(llmProfile.value), effectiveSessionID, runtimeOverrides, *resume)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ func TestRunControlsRejectSyntaxWithoutAllocatingState(t *testing.T) {
|
|||||||
{name: "blank session ID", args: func(roots stateTestRoots) []string {
|
{name: "blank session ID", args: func(roots stateTestRoots) []string {
|
||||||
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--session-id", ""}
|
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--session-id", ""}
|
||||||
}},
|
}},
|
||||||
|
{name: "blank LLM profile", args: func(roots stateTestRoots) []string {
|
||||||
|
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--llm-profile", ""}
|
||||||
|
}},
|
||||||
|
{name: "whitespace LLM profile", args: func(roots stateTestRoots) []string {
|
||||||
|
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--llm-profile", " \t "}
|
||||||
|
}},
|
||||||
{name: "multiple pipeline IDs", args: func(roots stateTestRoots) []string {
|
{name: "multiple pipeline IDs", args: func(roots stateTestRoots) []string {
|
||||||
return []string{"run", "sample", "extra", "--config", roots.config, "--input", roots.input}
|
return []string{"run", "sample", "extra", "--config", roots.config, "--input", roots.input}
|
||||||
}},
|
}},
|
||||||
@@ -253,7 +259,7 @@ func TestRunLLMProfileOverrideAndValidationUseInjectedBoundaries(t *testing.T) {
|
|||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
code := RunWithOptions([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass", "--llm-profile", "override-profile"}, &stdout, &stderr, opts)
|
code := RunWithOptions([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass", "--llm-profile", " override-profile "}, &stdout, &stderr, opts)
|
||||||
if code != 0 || stderr.Len() != 0 {
|
if code != 0 || stderr.Len() != 0 {
|
||||||
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -349,6 +350,12 @@ func ParseFileConfigYAML(data []byte) (FileConfig, error) {
|
|||||||
if err := decoder.Decode(&fileCfg); err != nil {
|
if err := decoder.Decode(&fileCfg); err != nil {
|
||||||
return FileConfig{}, fmt.Errorf("decode yaml: %w", err)
|
return FileConfig{}, fmt.Errorf("decode yaml: %w", err)
|
||||||
}
|
}
|
||||||
|
var trailing any
|
||||||
|
if err := decoder.Decode(&trailing); err == nil {
|
||||||
|
return FileConfig{}, fmt.Errorf("config must contain exactly one YAML document")
|
||||||
|
} else if err != io.EOF {
|
||||||
|
return FileConfig{}, fmt.Errorf("decode trailing yaml document: %w", err)
|
||||||
|
}
|
||||||
return fileCfg, nil
|
return fileCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,28 @@ func TestFileConfigMinimalVersion4AppliesOverDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFileConfigYAMLRejectsAdditionalDocuments(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "trailing whitespace", source: "version: 4\n\n \t", wantErr: false},
|
||||||
|
{name: "trailing comment", source: "version: 4\n# trailing comment\n", wantErr: false},
|
||||||
|
{name: "second valid document", source: "version: 4\n---\nversion: 4\n", wantErr: true},
|
||||||
|
{name: "second empty document", source: "version: 4\n---\n", wantErr: true},
|
||||||
|
{name: "second malformed document", source: "version: 4\n---\nversion: [\n", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := ParseFileConfigYAML([]byte(tt.source))
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("ParseFileConfigYAML() error = %v, want error=%t", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFilePipelineLLMProfileIsPresenceAwareAndDetached(t *testing.T) {
|
func TestFilePipelineLLMProfileIsPresenceAwareAndDetached(t *testing.T) {
|
||||||
const pipelineYAML = `version: 4
|
const pipelineYAML = `version: 4
|
||||||
pipelines:
|
pipelines:
|
||||||
|
|||||||
@@ -305,7 +305,10 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
|||||||
return ResolvedPipeline{}, err
|
return ResolvedPipeline{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
input := resolveBinding(profile.Input, "")
|
input, err := resolveBinding(profile.Input, "", fmt.Sprintf("pipeline %q input reference slot", pipelineID))
|
||||||
|
if err != nil {
|
||||||
|
return ResolvedPipeline{}, err
|
||||||
|
}
|
||||||
if input.Module == "" {
|
if input.Module == "" {
|
||||||
return ResolvedPipeline{}, fmt.Errorf("pipeline %q input module must not be empty", pipelineID)
|
return ResolvedPipeline{}, fmt.Errorf("pipeline %q input module must not be empty", pipelineID)
|
||||||
}
|
}
|
||||||
@@ -320,7 +323,10 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
|||||||
}
|
}
|
||||||
capabilities.add(inputModuleSpec.Provides...)
|
capabilities.add(inputModuleSpec.Provides...)
|
||||||
|
|
||||||
chunk := resolveBinding(profile.Chunk, DefaultChunkModule)
|
chunk, err := resolveBinding(profile.Chunk, DefaultChunkModule, fmt.Sprintf("pipeline %q chunk reference slot", pipelineID))
|
||||||
|
if err != nil {
|
||||||
|
return ResolvedPipeline{}, err
|
||||||
|
}
|
||||||
chunkSpec, err := chunkerSpec(catalog, chunk.Module)
|
chunkSpec, err := chunkerSpec(catalog, chunk.Module)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ResolvedPipeline{}, moduleLookupError(pipelineID, "", StageChunk, chunk.Module, err)
|
return ResolvedPipeline{}, moduleLookupError(pipelineID, "", StageChunk, chunk.Module, err)
|
||||||
@@ -363,6 +369,10 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ResolvedPipeline{}, err
|
return ResolvedPipeline{}, err
|
||||||
}
|
}
|
||||||
|
output, err := resolveBinding(profile.Output, DefaultOutputModule, fmt.Sprintf("pipeline %q output reference slot", pipelineID))
|
||||||
|
if err != nil {
|
||||||
|
return ResolvedPipeline{}, err
|
||||||
|
}
|
||||||
resolved := ResolvedPipeline{
|
resolved := ResolvedPipeline{
|
||||||
ID: pipelineID,
|
ID: pipelineID,
|
||||||
Input: input,
|
Input: input,
|
||||||
@@ -370,7 +380,7 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
|||||||
Chunk: chunk,
|
Chunk: chunk,
|
||||||
ChunkExecutionClass: chunkSpec.ExecutionClass,
|
ChunkExecutionClass: chunkSpec.ExecutionClass,
|
||||||
ChunkReferences: referenceTarget(StageChunk, "", chunk.Module, chunkReferences),
|
ChunkReferences: referenceTarget(StageChunk, "", chunk.Module, chunkReferences),
|
||||||
Output: resolveBinding(profile.Output, DefaultOutputModule),
|
Output: output,
|
||||||
}
|
}
|
||||||
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 {
|
||||||
@@ -485,13 +495,29 @@ func resolveArtifactLane(
|
|||||||
inherited capabilitySet,
|
inherited capabilitySet,
|
||||||
catalog ModuleCatalog,
|
catalog ModuleCatalog,
|
||||||
) (ResolvedArtifactLane, []ResolvedValidatorChain, capabilitySet, error) {
|
) (ResolvedArtifactLane, []ResolvedValidatorChain, capabilitySet, error) {
|
||||||
|
extract, err := resolveBinding(profile.Extract, "", fmt.Sprintf("pipeline %q step %q lane %q extract reference slot", pipelineID, stepID, laneID))
|
||||||
|
if err != nil {
|
||||||
|
return ResolvedArtifactLane{}, nil, nil, err
|
||||||
|
}
|
||||||
|
merge, err := resolveBinding(profile.Merge, DefaultMergeModule, fmt.Sprintf("pipeline %q step %q lane %q merge reference slot", pipelineID, stepID, laneID))
|
||||||
|
if err != nil {
|
||||||
|
return ResolvedArtifactLane{}, nil, nil, err
|
||||||
|
}
|
||||||
|
normalize, err := resolveBinding(profile.Normalize, DefaultNormalizeModule, fmt.Sprintf("pipeline %q step %q lane %q normalize reference slot", pipelineID, stepID, laneID))
|
||||||
|
if err != nil {
|
||||||
|
return ResolvedArtifactLane{}, nil, nil, err
|
||||||
|
}
|
||||||
|
validators, err := resolveBindings(profile.Validators, "", fmt.Sprintf("pipeline %q step %q lane %q validator reference slot", pipelineID, stepID, laneID))
|
||||||
|
if err != nil {
|
||||||
|
return ResolvedArtifactLane{}, nil, nil, err
|
||||||
|
}
|
||||||
lane := ResolvedArtifactLane{
|
lane := ResolvedArtifactLane{
|
||||||
StepID: strings.TrimSpace(stepID),
|
StepID: strings.TrimSpace(stepID),
|
||||||
ID: laneID,
|
ID: laneID,
|
||||||
Extract: resolveBinding(profile.Extract, ""),
|
Extract: extract,
|
||||||
Merge: resolveBinding(profile.Merge, DefaultMergeModule),
|
Merge: merge,
|
||||||
Normalize: resolveBinding(profile.Normalize, DefaultNormalizeModule),
|
Normalize: normalize,
|
||||||
Validators: resolveBindings(profile.Validators, ""),
|
Validators: validators,
|
||||||
}
|
}
|
||||||
if lane.Extract.Module == "" {
|
if lane.Extract.Module == "" {
|
||||||
return ResolvedArtifactLane{}, nil, nil, fmt.Errorf("pipeline %q lane %q extract module must not be empty", pipelineID, laneID)
|
return ResolvedArtifactLane{}, nil, nil, fmt.Errorf("pipeline %q lane %q extract module must not be empty", pipelineID, laneID)
|
||||||
@@ -516,6 +542,7 @@ func resolveArtifactLane(
|
|||||||
extractReferences := mergeReferenceMaps(profile.References, lane.Extract.References)
|
extractReferences := mergeReferenceMaps(profile.References, lane.Extract.References)
|
||||||
references, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
references, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
||||||
PipelineID: pipelineID,
|
PipelineID: pipelineID,
|
||||||
|
StepID: strings.TrimSpace(stepID),
|
||||||
LaneID: laneID,
|
LaneID: laneID,
|
||||||
Stage: StageExtract,
|
Stage: StageExtract,
|
||||||
Module: lane.Extract.Module,
|
Module: lane.Extract.Module,
|
||||||
@@ -541,6 +568,7 @@ func resolveArtifactLane(
|
|||||||
}
|
}
|
||||||
mergeReferences, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
mergeReferences, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
||||||
PipelineID: pipelineID,
|
PipelineID: pipelineID,
|
||||||
|
StepID: strings.TrimSpace(stepID),
|
||||||
LaneID: laneID,
|
LaneID: laneID,
|
||||||
Stage: StageMerge,
|
Stage: StageMerge,
|
||||||
Module: lane.Merge.Module,
|
Module: lane.Merge.Module,
|
||||||
@@ -566,6 +594,7 @@ func resolveArtifactLane(
|
|||||||
}
|
}
|
||||||
normalizeReferences, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
normalizeReferences, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
||||||
PipelineID: pipelineID,
|
PipelineID: pipelineID,
|
||||||
|
StepID: strings.TrimSpace(stepID),
|
||||||
LaneID: laneID,
|
LaneID: laneID,
|
||||||
Stage: StageNormalize,
|
Stage: StageNormalize,
|
||||||
Module: lane.Normalize.Module,
|
Module: lane.Normalize.Module,
|
||||||
@@ -925,7 +954,7 @@ func validatePipelineReferenceDefaults(
|
|||||||
lanesByID map[string]ArtifactLaneProfile,
|
lanesByID map[string]ArtifactLaneProfile,
|
||||||
catalog ModuleCatalog,
|
catalog ModuleCatalog,
|
||||||
) error {
|
) error {
|
||||||
normalizedPipelineReferences, err := normalizedReferenceMap(pipelineReferences, fmt.Sprintf("pipeline %q reference slot", pipelineID))
|
normalizedPipelineReferences, err := normalizeReferenceMap(pipelineReferences, fmt.Sprintf("pipeline %q reference slot", pipelineID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -939,38 +968,38 @@ func validatePipelineReferenceDefaults(
|
|||||||
}
|
}
|
||||||
for _, laneID := range sortedArtifactLaneProfileKeys(lanesByID) {
|
for _, laneID := range sortedArtifactLaneProfileKeys(lanesByID) {
|
||||||
laneProfile := lanesByID[laneID]
|
laneProfile := lanesByID[laneID]
|
||||||
extract := resolveBinding(laneProfile.Extract, "")
|
extractModule := resolveModuleKey(laneProfile.Extract.Module, "")
|
||||||
if extract.Module == "" {
|
if extractModule == "" {
|
||||||
return fmt.Errorf("pipeline %q lane %q extract module must not be empty", pipelineID, laneID)
|
return fmt.Errorf("pipeline %q lane %q extract module must not be empty", pipelineID, laneID)
|
||||||
}
|
}
|
||||||
extractSpec, err := extractorSpec(catalog, extract.Module)
|
extractSpec, err := extractorSpec(catalog, extractModule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return moduleLookupError(pipelineID, laneID, StageExtract, extract.Module, err)
|
return moduleLookupError(pipelineID, laneID, StageExtract, extractModule, err)
|
||||||
}
|
}
|
||||||
for _, slot := range extractSpec.ReferenceSlots {
|
for _, slot := range extractSpec.ReferenceSlots {
|
||||||
declaredByAnyTarget[slot.Name] = struct{}{}
|
declaredByAnyTarget[slot.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
merge := resolveBinding(laneProfile.Merge, DefaultMergeModule)
|
mergeModule := resolveModuleKey(laneProfile.Merge.Module, DefaultMergeModule)
|
||||||
var artifactType reflect.Type
|
var artifactType reflect.Type
|
||||||
artifactKind := extractSpec.ArtifactKind
|
artifactKind := extractSpec.ArtifactKind
|
||||||
if artifactKind != "" && catalog.Extractors != nil {
|
if artifactKind != "" && catalog.Extractors != nil {
|
||||||
if entry, ok := catalog.Extractors.typedEntry(extract.Module); ok {
|
if entry, ok := catalog.Extractors.typedEntry(extractModule); ok {
|
||||||
artifactType = entry.valueType
|
artifactType = entry.valueType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mergeSpec, err := mergerSpecForArtifact(catalog, merge.Module, artifactKind, artifactType)
|
mergeSpec, err := mergerSpecForArtifact(catalog, mergeModule, artifactKind, artifactType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return moduleLookupError(pipelineID, laneID, StageMerge, merge.Module, err)
|
return moduleLookupError(pipelineID, laneID, StageMerge, mergeModule, err)
|
||||||
}
|
}
|
||||||
for _, slot := range mergeSpec.ReferenceSlots {
|
for _, slot := range mergeSpec.ReferenceSlots {
|
||||||
declaredByAnyTarget[slot.Name] = struct{}{}
|
declaredByAnyTarget[slot.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize := resolveBinding(laneProfile.Normalize, DefaultNormalizeModule)
|
normalizeModule := resolveModuleKey(laneProfile.Normalize.Module, DefaultNormalizeModule)
|
||||||
normalizeSpec, err := normalizerSpecForArtifact(catalog, normalize.Module, artifactKind, artifactType)
|
normalizeSpec, err := normalizerSpecForArtifact(catalog, normalizeModule, artifactKind, artifactType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return moduleLookupError(pipelineID, laneID, StageNormalize, normalize.Module, err)
|
return moduleLookupError(pipelineID, laneID, StageNormalize, normalizeModule, err)
|
||||||
}
|
}
|
||||||
for _, slot := range normalizeSpec.ReferenceSlots {
|
for _, slot := range normalizeSpec.ReferenceSlots {
|
||||||
declaredByAnyTarget[slot.Name] = struct{}{}
|
declaredByAnyTarget[slot.Name] = struct{}{}
|
||||||
@@ -987,6 +1016,7 @@ func validatePipelineReferenceDefaults(
|
|||||||
|
|
||||||
type referenceResolutionTarget struct {
|
type referenceResolutionTarget struct {
|
||||||
PipelineID string
|
PipelineID string
|
||||||
|
StepID string
|
||||||
LaneID string
|
LaneID string
|
||||||
Stage ModuleStage
|
Stage ModuleStage
|
||||||
Module string
|
Module string
|
||||||
@@ -1046,7 +1076,7 @@ func resolveReferenceTargetBindings(target referenceResolutionTarget) ([]Referen
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedPipelineReferences, err := normalizedReferenceMap(target.PipelineReferences, fmt.Sprintf("pipeline %q reference slot", target.PipelineID))
|
normalizedPipelineReferences, err := normalizeReferenceMap(target.PipelineReferences, fmt.Sprintf("pipeline %q reference slot", target.PipelineID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1059,7 +1089,7 @@ func resolveReferenceTargetBindings(target referenceResolutionTarget) ([]Referen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedLocalReferences, err := normalizedReferenceMap(target.LocalReferences, referenceTargetSlotLabel(target))
|
normalizedLocalReferences, err := normalizeReferenceMap(target.LocalReferences, referenceTargetSlotLabel(target))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1166,6 +1196,9 @@ func sortedReferenceBindings(bindings map[string]ReferenceBinding) []ReferenceBi
|
|||||||
|
|
||||||
func referenceTargetErrorContext(target referenceResolutionTarget) string {
|
func referenceTargetErrorContext(target referenceResolutionTarget) string {
|
||||||
if target.LaneID != "" {
|
if target.LaneID != "" {
|
||||||
|
if target.StepID != "" {
|
||||||
|
return fmt.Sprintf("pipeline %q step %q lane %q %s module %q", target.PipelineID, target.StepID, target.LaneID, target.Stage, target.Module)
|
||||||
|
}
|
||||||
return fmt.Sprintf("pipeline %q lane %q %s module %q", target.PipelineID, target.LaneID, target.Stage, target.Module)
|
return fmt.Sprintf("pipeline %q lane %q %s module %q", target.PipelineID, target.LaneID, target.Stage, target.Module)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("pipeline %q %s module %q", target.PipelineID, target.Stage, target.Module)
|
return fmt.Sprintf("pipeline %q %s module %q", target.PipelineID, target.Stage, target.Module)
|
||||||
@@ -1173,17 +1206,22 @@ func referenceTargetErrorContext(target referenceResolutionTarget) string {
|
|||||||
|
|
||||||
func referenceTargetSlotLabel(target referenceResolutionTarget) string {
|
func referenceTargetSlotLabel(target referenceResolutionTarget) string {
|
||||||
if target.LaneID != "" {
|
if target.LaneID != "" {
|
||||||
|
if target.StepID != "" {
|
||||||
|
return fmt.Sprintf("pipeline %q step %q lane %q %s reference slot", target.PipelineID, target.StepID, target.LaneID, target.Stage)
|
||||||
|
}
|
||||||
return fmt.Sprintf("pipeline %q lane %q %s reference slot", target.PipelineID, target.LaneID, target.Stage)
|
return fmt.Sprintf("pipeline %q lane %q %s reference slot", target.PipelineID, target.LaneID, target.Stage)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("pipeline %q %s reference slot", target.PipelineID, target.Stage)
|
return fmt.Sprintf("pipeline %q %s reference slot", target.PipelineID, target.Stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizedReferenceMap(values map[string]ReferenceSource, keyName string) (map[string]ReferenceSource, error) {
|
func normalizeReferenceMap(values map[string]ReferenceSource, keyName string) (map[string]ReferenceSource, error) {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
out := make(map[string]ReferenceSource, len(values))
|
out := make(map[string]ReferenceSource, len(values))
|
||||||
for rawSlotName, rawSource := range values {
|
rawSlotNames := sortedStringMapKeys(values)
|
||||||
|
for _, rawSlotName := range rawSlotNames {
|
||||||
|
rawSource := values[rawSlotName]
|
||||||
slotName := strings.TrimSpace(rawSlotName)
|
slotName := strings.TrimSpace(rawSlotName)
|
||||||
if slotName == "" {
|
if slotName == "" {
|
||||||
return nil, fmt.Errorf("%s must not be empty", keyName)
|
return nil, fmt.Errorf("%s must not be empty", keyName)
|
||||||
@@ -1236,20 +1274,29 @@ func sortedReferenceBindingKeys(values map[string]ReferenceBinding) []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveBinding(binding ModuleBinding, defaultModule string) ModuleBinding {
|
func resolveBinding(binding ModuleBinding, defaultModule string, referenceSlotLabel string) (ModuleBinding, error) {
|
||||||
module := strings.TrimSpace(binding.Module)
|
module := resolveModuleKey(binding.Module, defaultModule)
|
||||||
if module == "" {
|
|
||||||
module = defaultModule
|
|
||||||
}
|
|
||||||
llmProfile := strings.TrimSpace(binding.LLMProfile)
|
llmProfile := strings.TrimSpace(binding.LLMProfile)
|
||||||
|
references, err := normalizeReferenceMap(binding.References, referenceSlotLabel)
|
||||||
|
if err != nil {
|
||||||
|
return ModuleBinding{}, err
|
||||||
|
}
|
||||||
return ModuleBinding{
|
return ModuleBinding{
|
||||||
Module: module,
|
Module: module,
|
||||||
LLMProfile: llmProfile,
|
LLMProfile: llmProfile,
|
||||||
Retries: binding.Retries,
|
Retries: binding.Retries,
|
||||||
Options: cloneOptions(binding.Options),
|
Options: cloneOptions(binding.Options),
|
||||||
References: normalizeReferenceMap(binding.References),
|
References: references,
|
||||||
Validators: cloneValidatorOverride(binding.Validators),
|
Validators: cloneValidatorOverride(binding.Validators),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveModuleKey(module string, defaultModule string) string {
|
||||||
|
module = strings.TrimSpace(module)
|
||||||
|
if module == "" {
|
||||||
|
return defaultModule
|
||||||
}
|
}
|
||||||
|
return module
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyEffectiveLLMProfiles(resolved *ResolvedPipeline, pipelineProfile, overrideProfile string) error {
|
func applyEffectiveLLMProfiles(resolved *ResolvedPipeline, pipelineProfile, overrideProfile string) error {
|
||||||
@@ -1312,17 +1359,20 @@ func applyEffectiveLLMProfiles(resolved *ResolvedPipeline, pipelineProfile, over
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveBindings(bindings []ModuleBinding, defaultModule string) []ModuleBinding {
|
func resolveBindings(bindings []ModuleBinding, defaultModule string, referenceSlotLabel string) ([]ModuleBinding, error) {
|
||||||
if len(bindings) == 0 {
|
if len(bindings) == 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved := make([]ModuleBinding, 0, len(bindings))
|
resolved := make([]ModuleBinding, 0, len(bindings))
|
||||||
for _, binding := range bindings {
|
for index, binding := range bindings {
|
||||||
resolvedBinding := resolveBinding(binding, defaultModule)
|
resolvedBinding, err := resolveBinding(binding, defaultModule, fmt.Sprintf("%s %d", referenceSlotLabel, index))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
resolved = append(resolved, resolvedBinding)
|
resolved = append(resolved, resolvedBinding)
|
||||||
}
|
}
|
||||||
return resolved
|
return resolved, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneOptions(options map[string]any) map[string]any {
|
func cloneOptions(options map[string]any) map[string]any {
|
||||||
@@ -1358,25 +1408,6 @@ func cloneOptionValue(value any) any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeReferenceMap(values map[string]ReferenceSource) map[string]ReferenceSource {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := make(map[string]ReferenceSource, len(values))
|
|
||||||
keys := make([]string, 0, len(values))
|
|
||||||
rawByNormalized := make(map[string]string, len(values))
|
|
||||||
for rawKey := range values {
|
|
||||||
key := strings.TrimSpace(rawKey)
|
|
||||||
rawByNormalized[key] = rawKey
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, key := range keys {
|
|
||||||
out[key] = cloneReferenceSource(values[rawByNormalized[key]])
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectedArtifactLanes(pipelineID string, artifacts map[string]ArtifactLaneProfile, options ResolveOptions) (map[string]ArtifactLaneProfile, []string, error) {
|
func selectedArtifactLanes(pipelineID string, artifacts map[string]ArtifactLaneProfile, options ResolveOptions) (map[string]ArtifactLaneProfile, []string, error) {
|
||||||
lanesByID := make(map[string]ArtifactLaneProfile, len(artifacts))
|
lanesByID := make(map[string]ArtifactLaneProfile, len(artifacts))
|
||||||
for rawLaneID, lane := range artifacts {
|
for rawLaneID, lane := range artifacts {
|
||||||
|
|||||||
@@ -159,6 +159,85 @@ func TestResolvePipelineAppliesDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolvePipelineRejectsReferenceSlotCollisionsAfterTrimming(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
configure func(*PipelineProfile)
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "chunk",
|
||||||
|
configure: func(profile *PipelineProfile) {
|
||||||
|
profile.Chunk = ModuleBinding{Module: DefaultChunkModule, References: map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}}
|
||||||
|
},
|
||||||
|
want: []string{`pipeline "references"`, "chunk", `reference slot "rules" is duplicated after trimming`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extract",
|
||||||
|
configure: func(profile *PipelineProfile) {
|
||||||
|
lane := profile.Artifacts["lane"]
|
||||||
|
lane.Extract.References = map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}
|
||||||
|
profile.Artifacts["lane"] = lane
|
||||||
|
},
|
||||||
|
want: []string{`pipeline "references"`, `step "default"`, `lane "lane"`, "extract", `reference slot "rules" is duplicated after trimming`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge",
|
||||||
|
configure: func(profile *PipelineProfile) {
|
||||||
|
lane := profile.Artifacts["lane"]
|
||||||
|
lane.Merge = ModuleBinding{Module: DefaultMergeModule, References: map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}}
|
||||||
|
profile.Artifacts["lane"] = lane
|
||||||
|
},
|
||||||
|
want: []string{`pipeline "references"`, `step "default"`, `lane "lane"`, "merge", `reference slot "rules" is duplicated after trimming`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normalize",
|
||||||
|
configure: func(profile *PipelineProfile) {
|
||||||
|
lane := profile.Artifacts["lane"]
|
||||||
|
lane.Normalize = ModuleBinding{Module: DefaultNormalizeModule, References: map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}}
|
||||||
|
profile.Artifacts["lane"] = lane
|
||||||
|
},
|
||||||
|
want: []string{`pipeline "references"`, `step "default"`, `lane "lane"`, "normalize", `reference slot "rules" is duplicated after trimming`},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
profile := PipelineProfile{
|
||||||
|
ID: "references",
|
||||||
|
Input: Binding("text"),
|
||||||
|
Artifacts: map[string]ArtifactLaneProfile{
|
||||||
|
"lane": {Extract: Binding("event-extractor")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tt.configure(&profile)
|
||||||
|
_, err := ResolvePipeline(profile, ResolveOptions{}, newProfileCatalog(t))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ResolvePipeline() error = nil, want normalized reference collision")
|
||||||
|
}
|
||||||
|
for _, fragment := range tt.want {
|
||||||
|
if !strings.Contains(err.Error(), fragment) {
|
||||||
|
t.Fatalf("ResolvePipeline() error = %q, want context %q", err, fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolvePipelineRejectsEmptyReferenceSlotAfterTrimming(t *testing.T) {
|
||||||
|
_, err := ResolvePipeline(PipelineProfile{
|
||||||
|
ID: "references",
|
||||||
|
Input: Binding("text"),
|
||||||
|
Chunk: ModuleBinding{Module: DefaultChunkModule, References: map[string]ReferenceSource{
|
||||||
|
" \t ": ExternalReference("rules.md"),
|
||||||
|
}},
|
||||||
|
Artifacts: map[string]ArtifactLaneProfile{
|
||||||
|
"lane": {Extract: Binding("event-extractor")},
|
||||||
|
},
|
||||||
|
}, ResolveOptions{}, newProfileCatalog(t))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), `pipeline "references" chunk reference slot must not be empty`) {
|
||||||
|
t.Fatalf("ResolvePipeline() error = %v, want contextual empty reference-slot rejection", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolvePipelineAppliesEffectiveLLMProfiles(t *testing.T) {
|
func TestResolvePipelineAppliesEffectiveLLMProfiles(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -71,7 +71,11 @@ func normalizeValidatorChainMapping(mapping ValidatorChainMapping) (ValidatorCha
|
|||||||
if strings.TrimSpace(validator.Module) == "" {
|
if strings.TrimSpace(validator.Module) == "" {
|
||||||
return ValidatorChainMapping{}, fmt.Errorf("validator chain for %q %q has empty validator key at index %d", normalized.Stage, normalized.Module, i)
|
return ValidatorChainMapping{}, fmt.Errorf("validator chain for %q %q has empty validator key at index %d", normalized.Stage, normalized.Module, i)
|
||||||
}
|
}
|
||||||
normalized.Validators[i] = resolveBinding(validator, "")
|
resolved, err := resolveBinding(validator, "", fmt.Sprintf("validator chain for %q %q validator %d reference slot", normalized.Stage, normalized.Module, i))
|
||||||
|
if err != nil {
|
||||||
|
return ValidatorChainMapping{}, err
|
||||||
|
}
|
||||||
|
normalized.Validators[i] = resolved
|
||||||
}
|
}
|
||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user