Add merge references and retry config
This commit is contained in:
@@ -99,7 +99,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
||||
referenceFlags := stringListFlag{}
|
||||
withoutReferenceFlags := stringListFlag{}
|
||||
fs.Var(&sessionID, "session-id", "prompt session identifier")
|
||||
fs.Var(&referenceFlags, "reference", "reference binding, as slot=path, chunk.slot=path, lane.slot=path, lane.extract.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")
|
||||
fs.Var(&withoutReferenceFlags, "without-reference", "unbind a reference, using the same selector forms as --reference")
|
||||
if err := validateRunFlagValues(args); err != nil {
|
||||
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
||||
@@ -492,6 +492,7 @@ func effectiveLLMProfileIDs(resolved pipeline.ResolvedPipeline) []string {
|
||||
add(resolved.Chunk)
|
||||
for _, lane := range resolved.ArtifactLanes {
|
||||
add(lane.Extract)
|
||||
add(lane.Merge)
|
||||
add(lane.Normalize)
|
||||
}
|
||||
ids := make([]string, 0, len(seen))
|
||||
@@ -834,17 +835,20 @@ func parseReferenceSelector(raw string, flagName string) (cliReferenceSelector,
|
||||
if first == string(pipeline.StageChunk) {
|
||||
return cliReferenceSelector{Stage: pipeline.StageChunk, SlotName: slotName}, nil
|
||||
}
|
||||
if first == string(pipeline.StageMerge) {
|
||||
return cliReferenceSelector{Stage: pipeline.StageMerge, SlotName: slotName}, nil
|
||||
}
|
||||
return cliReferenceSelector{LaneID: first, SlotName: slotName}, nil
|
||||
case 3:
|
||||
laneID := strings.TrimSpace(parts[0])
|
||||
stage := pipeline.ModuleStage(strings.TrimSpace(parts[1]))
|
||||
slotName := strings.TrimSpace(parts[2])
|
||||
if stage != pipeline.StageExtract && stage != pipeline.StageNormalize {
|
||||
return cliReferenceSelector{}, fmt.Errorf("%s lane-qualified selector must use lane.extract.slot or lane.normalize.slot", flagName)
|
||||
if stage != pipeline.StageExtract && stage != pipeline.StageMerge && stage != pipeline.StageNormalize {
|
||||
return cliReferenceSelector{}, fmt.Errorf("%s lane-qualified selector must use lane.extract.slot, lane.merge.slot, or lane.normalize.slot", flagName)
|
||||
}
|
||||
return cliReferenceSelector{LaneID: laneID, Stage: stage, SlotName: slotName}, nil
|
||||
default:
|
||||
return cliReferenceSelector{}, fmt.Errorf("%s must use slot, chunk.slot, lane.slot, lane.extract.slot, or lane.normalize.slot", flagName)
|
||||
return cliReferenceSelector{}, fmt.Errorf("%s must use slot, chunk.slot, merge.slot, lane.slot, lane.extract.slot, lane.merge.slot, or lane.normalize.slot", flagName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,7 +948,7 @@ func selectedReferenceTargets(cfg config.Config, pipelineID string, only []strin
|
||||
}
|
||||
sort.Strings(selectedIDs)
|
||||
|
||||
targets := make([]selectedReferenceTarget, 0, 1+len(selectedIDs)*2)
|
||||
targets := make([]selectedReferenceTarget, 0, 1+len(selectedIDs)*3)
|
||||
chunk := pipeline.Binding(profile.Chunk.Module)
|
||||
chunk.Module = strings.TrimSpace(profile.Chunk.Module)
|
||||
if chunk.Module == "" {
|
||||
@@ -977,6 +981,21 @@ func selectedReferenceTargets(cfg config.Config, pipelineID string, only []strin
|
||||
slots: referenceSlotSet(extractSpec.ReferenceSlots),
|
||||
})
|
||||
|
||||
mergeModule := strings.TrimSpace(lane.Merge.Module)
|
||||
if mergeModule == "" {
|
||||
mergeModule = pipeline.DefaultMergeModule
|
||||
}
|
||||
mergeSpec, err := cliReferenceMergerSpec(catalog, mergeModule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pipeline %q lane %q merge module %q: %w", strings.TrimSpace(pipelineID), laneID, mergeModule, err)
|
||||
}
|
||||
targets = append(targets, selectedReferenceTarget{
|
||||
laneID: laneID,
|
||||
stage: pipeline.StageMerge,
|
||||
module: mergeModule,
|
||||
slots: referenceSlotSet(mergeSpec.ReferenceSlots),
|
||||
})
|
||||
|
||||
normalizeModule := strings.TrimSpace(lane.Normalize.Module)
|
||||
if normalizeModule == "" {
|
||||
normalizeModule = pipeline.DefaultNormalizeModule
|
||||
@@ -1027,6 +1046,17 @@ func cliReferenceExtractorSpec(catalog pipeline.ModuleCatalog, module string) (p
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func cliReferenceMergerSpec(catalog pipeline.ModuleCatalog, module string) (pipeline.ModuleSpec, error) {
|
||||
if catalog.Mergers == nil {
|
||||
return pipeline.ModuleSpec{}, fmt.Errorf("module %q is not registered", module)
|
||||
}
|
||||
spec, ok := catalog.Mergers.Spec(module)
|
||||
if !ok {
|
||||
return pipeline.ModuleSpec{}, fmt.Errorf("module %q is not registered", module)
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func cliReferenceNormalizerSpec(catalog pipeline.ModuleCatalog, module string) (pipeline.ModuleSpec, error) {
|
||||
if catalog.Normalizers == nil {
|
||||
return pipeline.ModuleSpec{}, fmt.Errorf("module %q is not registered", module)
|
||||
@@ -1063,7 +1093,10 @@ func resolveCLIReferenceTarget(targets []selectedReferenceTarget, selector cliRe
|
||||
}
|
||||
return selectedReferenceTarget{}, fmt.Errorf("reference chunk target is not selected")
|
||||
}
|
||||
if selector.Stage == pipeline.StageExtract || selector.Stage == pipeline.StageNormalize {
|
||||
if selector.Stage == pipeline.StageExtract || selector.Stage == pipeline.StageMerge || selector.Stage == pipeline.StageNormalize {
|
||||
if selector.LaneID == "" && selector.Stage == pipeline.StageMerge {
|
||||
return resolveCLIReferenceStageTarget(targets, selector.Stage, slotName)
|
||||
}
|
||||
for _, target := range targets {
|
||||
if target.laneID == selector.LaneID && target.stage == selector.Stage {
|
||||
if _, ok := target.slots[slotName]; !ok {
|
||||
@@ -1080,6 +1113,26 @@ func resolveCLIReferenceTarget(targets []selectedReferenceTarget, selector cliRe
|
||||
return resolveCLIReferenceFlatTarget(targets, slotName)
|
||||
}
|
||||
|
||||
func resolveCLIReferenceStageTarget(targets []selectedReferenceTarget, stage pipeline.ModuleStage, slotName string) (selectedReferenceTarget, error) {
|
||||
matches := make([]selectedReferenceTarget, 0, 2)
|
||||
for _, target := range targets {
|
||||
if target.stage != stage {
|
||||
continue
|
||||
}
|
||||
if _, ok := target.slots[slotName]; ok {
|
||||
matches = append(matches, target)
|
||||
}
|
||||
}
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
return selectedReferenceTarget{}, fmt.Errorf("reference slot %q is not declared by any selected %s target", slotName, stage)
|
||||
case 1:
|
||||
return matches[0], nil
|
||||
default:
|
||||
return selectedReferenceTarget{}, fmt.Errorf("reference slot %q is declared by multiple selected %s targets (%s); use a more specific selector such as %s", slotName, stage, targetList(matches), selectorSuggestions(matches, slotName))
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCLIReferenceLaneTarget(targets []selectedReferenceTarget, laneID string, slotName string) (selectedReferenceTarget, error) {
|
||||
laneSelected := false
|
||||
matches := make([]selectedReferenceTarget, 0, 2)
|
||||
@@ -1101,7 +1154,7 @@ func resolveCLIReferenceLaneTarget(targets []selectedReferenceTarget, laneID str
|
||||
case 1:
|
||||
return matches[0], nil
|
||||
default:
|
||||
return selectedReferenceTarget{}, fmt.Errorf("reference slot %q is declared by multiple selected targets in lane %q (%s); use %s.extract.%s or %s.normalize.%s", slotName, laneID, targetList(matches), laneID, slotName, laneID, slotName)
|
||||
return selectedReferenceTarget{}, fmt.Errorf("reference slot %q is declared by multiple selected targets in lane %q (%s); use a more specific selector such as %s", slotName, laneID, targetList(matches), selectorSuggestions(matches, slotName))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -816,7 +816,7 @@ pipelines:
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateIgnoresNonLLMStageScriptoriumProfiles(t *testing.T) {
|
||||
func TestRunConfigValidateIncludesMergeAndIgnoresNonLLMStageScriptoriumProfiles(t *testing.T) {
|
||||
profilePath := writeScriptoriumProfileFile(t, "known", "http://profile.test/v1", "test-model")
|
||||
configPath := writeTestConfig(t, `version: 2
|
||||
scriptorium:
|
||||
@@ -843,11 +843,11 @@ pipelines:
|
||||
Catalog: fakeCatalog(t),
|
||||
})
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("RunWithOptions() code = %d, stderr=%q, want success", code, stderr.String())
|
||||
if code != 1 {
|
||||
t.Fatalf("RunWithOptions() code = %d, want failure for missing merge profile", code)
|
||||
}
|
||||
if strings.Contains(stderr.String(), "missing-") {
|
||||
t.Fatalf("stderr = %q, want non-LLM stage profiles ignored", stderr.String())
|
||||
if !strings.Contains(stderr.String(), "Scriptorium profile") || !strings.Contains(stderr.String(), "missing-merge") {
|
||||
t.Fatalf("stderr = %q, want missing merge profile error", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,7 +867,7 @@ func TestEffectiveLLMProfileIDsUsesLLMCapableStagesOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
got := effectiveLLMProfileIDs(resolved)
|
||||
want := []string{"chunk-profile", "extract-profile", "normalize-profile"}
|
||||
want := []string{"chunk-profile", "extract-profile", "merge-profile", "normalize-profile"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("effectiveLLMProfileIDs() = %#v, want %#v", got, want)
|
||||
}
|
||||
@@ -1166,6 +1166,81 @@ func TestRunPipelineReferenceFlagBindsExplicitNormalizeSlot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPipelineReferenceFlagBindsExplicitMergeSlot(t *testing.T) {
|
||||
configPath := writeTestConfig(t, testConfigYAML("example", "events"))
|
||||
inputPath := filepath.Join(t.TempDir(), "missing.json")
|
||||
referencePath := writeFile(t, "merge.md", "Merge notes\n")
|
||||
diagnosticsDir := t.TempDir()
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := RunWithOptions([]string{
|
||||
"run", "example",
|
||||
"--config", configPath,
|
||||
"--input", inputPath,
|
||||
"--diagnostics-dir", diagnosticsDir,
|
||||
"--reference", "events.merge.notes=" + referencePath,
|
||||
}, &stdout, &stderr, Options{
|
||||
Catalog: fakeCatalog(t, pipeline.ModuleSpec{
|
||||
Key: "appendorder",
|
||||
Stage: pipeline.StageMerge,
|
||||
Requires: []string{"artifact"},
|
||||
Provides: []string{"merged"},
|
||||
ReferenceSlots: []contracts.ReferenceSlot{
|
||||
{Name: "notes"},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if code != 1 || !strings.Contains(stderr.String(), "read input") {
|
||||
t.Fatalf("RunWithOptions() code = %d stderr=%q, want read input failure after resolution", code, stderr.String())
|
||||
}
|
||||
resolved := readResolvedPipeline(t, diagnosticsDir)
|
||||
refs := resolved.ArtifactLanes[0].MergeReferences.Bindings
|
||||
want := []pipeline.ReferenceBinding{
|
||||
{LaneID: "events", SlotName: "notes", Source: referencePath, BindingSource: contracts.ReferenceBindingSourceCLI},
|
||||
}
|
||||
if !reflect.DeepEqual(refs, want) {
|
||||
t.Fatalf("merge references = %#v, want %#v", refs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPipelineReferenceFlagBindsUnambiguousMergeSlot(t *testing.T) {
|
||||
configPath := writeTestConfig(t, testConfigYAML("example", "events"))
|
||||
inputPath := filepath.Join(t.TempDir(), "missing.json")
|
||||
referencePath := writeFile(t, "merge.md", "Merge notes\n")
|
||||
diagnosticsDir := t.TempDir()
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := RunWithOptions([]string{
|
||||
"run", "example",
|
||||
"--config", configPath,
|
||||
"--input", inputPath,
|
||||
"--diagnostics-dir", diagnosticsDir,
|
||||
"--reference", "merge.notes=" + referencePath,
|
||||
}, &stdout, &stderr, Options{
|
||||
Catalog: fakeCatalog(t, pipeline.ModuleSpec{
|
||||
Key: "appendorder",
|
||||
Stage: pipeline.StageMerge,
|
||||
Requires: []string{"artifact"},
|
||||
Provides: []string{"merged"},
|
||||
ReferenceSlots: []contracts.ReferenceSlot{
|
||||
{Name: "notes"},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if code != 1 || !strings.Contains(stderr.String(), "read input") {
|
||||
t.Fatalf("RunWithOptions() code = %d stderr=%q, want read input failure after resolution", code, stderr.String())
|
||||
}
|
||||
resolved := readResolvedPipeline(t, diagnosticsDir)
|
||||
refs := resolved.ArtifactLanes[0].MergeReferences.Bindings
|
||||
if len(refs) != 1 || refs[0].Source != referencePath || refs[0].LaneID != "events" {
|
||||
t.Fatalf("merge references = %#v, want unambiguous merge binding", refs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPipelineReferenceFlagBindsFlatSlotAcrossOneTarget(t *testing.T) {
|
||||
configPath := writeTestConfig(t, testConfigYAML("example", "events"))
|
||||
inputPath := filepath.Join(t.TempDir(), "missing.json")
|
||||
@@ -1366,8 +1441,8 @@ func TestRunPipelineReferenceFlagsRejectMalformedValues(t *testing.T) {
|
||||
{name: "missing equals", args: []string{"--reference", "roster"}, want: "slot=path"},
|
||||
{name: "empty path", args: []string{"--reference", "roster="}, want: "path must not be empty"},
|
||||
{name: "empty slot", args: []string{"--reference", "=./roster.yml"}, want: "slot must not be empty"},
|
||||
{name: "unsupported explicit stage", args: []string{"--reference", "a.b.c=./roster.yml"}, want: "lane.extract.slot or lane.normalize.slot"},
|
||||
{name: "too many selector parts", args: []string{"--reference", "a.b.c.d=./roster.yml"}, want: "slot, chunk.slot, lane.slot, lane.extract.slot, or lane.normalize.slot"},
|
||||
{name: "unsupported explicit stage", args: []string{"--reference", "a.b.c=./roster.yml"}, want: "lane.extract.slot, lane.merge.slot, or lane.normalize.slot"},
|
||||
{name: "too many selector parts", args: []string{"--reference", "a.b.c.d=./roster.yml"}, want: "slot, chunk.slot, merge.slot, lane.slot, lane.extract.slot, lane.merge.slot, or lane.normalize.slot"},
|
||||
{name: "unbind with equals", args: []string{"--without-reference", "roster=./roster.yml"}, want: "without =path"},
|
||||
}
|
||||
|
||||
@@ -1393,9 +1468,10 @@ func TestRunPipelineReferenceFlagsRejectMalformedValues(t *testing.T) {
|
||||
|
||||
func TestRunPipelineWithoutReferenceRemovesConfigBindingsForEligibleTargets(t *testing.T) {
|
||||
configPath := writeTestConfig(t, testConfigYAMLWithPipelineReferences("example", "events", map[string]string{
|
||||
"context": "./config-context.md",
|
||||
"notes": "./config-notes.md",
|
||||
"roster": "./config-roster.yml",
|
||||
"context": "./config-context.md",
|
||||
"merge_notes": "./config-merge.md",
|
||||
"notes": "./config-notes.md",
|
||||
"roster": "./config-roster.yml",
|
||||
}))
|
||||
inputPath := filepath.Join(t.TempDir(), "missing.json")
|
||||
diagnosticsDir := t.TempDir()
|
||||
@@ -1409,6 +1485,7 @@ func TestRunPipelineWithoutReferenceRemovesConfigBindingsForEligibleTargets(t *t
|
||||
"--diagnostics-dir", diagnosticsDir,
|
||||
"--without-reference", "chunk.context",
|
||||
"--without-reference", "events.extract.roster",
|
||||
"--without-reference", "events.merge.merge_notes",
|
||||
"--without-reference", "events.normalize.notes",
|
||||
}, &stdout, &stderr, Options{
|
||||
Catalog: fakeCatalog(t,
|
||||
@@ -1421,6 +1498,15 @@ func TestRunPipelineWithoutReferenceRemovesConfigBindingsForEligibleTargets(t *t
|
||||
{Name: "context"},
|
||||
},
|
||||
},
|
||||
pipeline.ModuleSpec{
|
||||
Key: "appendorder",
|
||||
Stage: pipeline.StageMerge,
|
||||
Requires: []string{"artifact"},
|
||||
Provides: []string{"merged"},
|
||||
ReferenceSlots: []contracts.ReferenceSlot{
|
||||
{Name: "merge_notes"},
|
||||
},
|
||||
},
|
||||
pipeline.ModuleSpec{
|
||||
Key: "fake/extract",
|
||||
Stage: pipeline.StageExtract,
|
||||
@@ -1452,6 +1538,9 @@ func TestRunPipelineWithoutReferenceRemovesConfigBindingsForEligibleTargets(t *t
|
||||
if refs := resolved.ArtifactLanes[0].ExtractReferences.Bindings; len(refs) != 0 {
|
||||
t.Fatalf("extract references = %#v, want none", refs)
|
||||
}
|
||||
if refs := resolved.ArtifactLanes[0].MergeReferences.Bindings; len(refs) != 0 {
|
||||
t.Fatalf("merge references = %#v, want none", refs)
|
||||
}
|
||||
if refs := resolved.ArtifactLanes[0].NormalizeReferences.Bindings; len(refs) != 0 {
|
||||
t.Fatalf("normalize references = %#v, want none", refs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user