Add pipeline inheritance coverage and guidance
This commit is contained in:
@@ -91,6 +91,42 @@ append a pipeline's `steps` sequence. An overlay replaces sequences as defined
|
||||
by ConfigKit, so changing an individual ordered binding requires supplying the
|
||||
whole replacement sequence.
|
||||
|
||||
## Sibling Inheritance
|
||||
|
||||
ConfigKit v0.5.0 supports native `extends` composition in mappings. A directive
|
||||
names exactly one immediate sibling in the same mapping; it is not a path or a
|
||||
cross-scope lookup. Imports, the selected overlay, and explicit overrides are
|
||||
composed before inheritance, which runs before typed decoding and normal
|
||||
Notarius validation. Every definition, including a parent and definitions not
|
||||
selected for a run, remains subject to ordinary validation.
|
||||
|
||||
Child mappings recursively overlay parent mappings. A child sequence, scalar,
|
||||
or null replaces the inherited value. This makes pipeline inheritance useful
|
||||
for reusing a complete pipeline while replacing whole ordered lists:
|
||||
|
||||
~~~yaml
|
||||
pipelines:
|
||||
production:
|
||||
input: {module: seriatim}
|
||||
llm_profile: dnd-extraction
|
||||
# Other complete pipeline settings and ordered steps...
|
||||
development:
|
||||
extends: production
|
||||
structured_output_repair_attempts: 0
|
||||
~~~
|
||||
|
||||
Pipeline names are siblings within `pipelines`; nested mappings such as named
|
||||
artifact lanes also have their own sibling namespace. Step entries are a
|
||||
sequence, so a step cannot extend another step. Replacing `steps` or a validator
|
||||
chain supplies the complete replacement sequence; entries are not appended or
|
||||
merged by identifier.
|
||||
|
||||
`extends` is now a reserved key in every input mapping, including dynamic
|
||||
module options and named maps. Existing ordinary data or entry names using that
|
||||
key must be renamed along with any consumer that reads them. Configuration
|
||||
overlays, pipeline inheritance, and PromptKit profile `base_profile`
|
||||
inheritance are separate mechanisms.
|
||||
|
||||
## PromptKit Profiles
|
||||
|
||||
The optional **promptkit** object selects one source of profile definitions and
|
||||
|
||||
@@ -108,6 +108,8 @@ adjustments. Leave new inheritance coverage to the following stages.
|
||||
|
||||
## Stage 2 — Prove Pipeline Composition And Runtime Conversion
|
||||
|
||||
> Completion: complete.
|
||||
|
||||
Goal: establish whole-pipeline reuse through the real configuration boundary.
|
||||
|
||||
Inspect `load.go`, `document.go`, `loaded_config.go`, `pipeline_document.go`,
|
||||
|
||||
133
internal/core/config/inheritance_test.go
Normal file
133
internal/core/config/inheritance_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
func TestPipelineInheritancePreservesCompositionAndReplacesSteps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
childSteps string
|
||||
wantStepIDs []string
|
||||
}{
|
||||
{
|
||||
name: "inherits ordered steps",
|
||||
wantStepIDs: []string{"prepare", "summarize"},
|
||||
},
|
||||
{
|
||||
name: "replaces ordered steps",
|
||||
childSteps: `
|
||||
steps:
|
||||
- id: replacement
|
||||
artifacts:
|
||||
summary:
|
||||
extract: {module: extract}`,
|
||||
wantStepIDs: []string{"replacement"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
source := `version: 5
|
||||
pipelines:
|
||||
parent:
|
||||
input: {module: input}
|
||||
structured_output_repair_attempts: 2
|
||||
steps:
|
||||
- id: prepare
|
||||
artifacts:
|
||||
score:
|
||||
extract: {module: extract, retries: 2}
|
||||
- id: summarize
|
||||
artifacts:
|
||||
notes:
|
||||
extract: {module: extract}
|
||||
child:
|
||||
extends: parent
|
||||
llm_profile: local
|
||||
structured_output_repair_attempts: 0` + test.childSteps + "\n"
|
||||
loaded, err := loadConfigSource(configkit.Bytes("inheritance.yml", []byte(source)), LoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadConfigSource() error = %v", err)
|
||||
}
|
||||
|
||||
parent := loaded.Config.Pipelines["parent"]
|
||||
child := loaded.Config.Pipelines["child"]
|
||||
if parent.Input.Module != "input" || child.Input.Module != "input" {
|
||||
t.Fatalf("parent/child inputs = %#v / %#v", parent.Input, child.Input)
|
||||
}
|
||||
if child.LLMProfile != "local" ||
|
||||
child.StructuredOutputRepairAttempts == nil || *child.StructuredOutputRepairAttempts != 0 {
|
||||
t.Fatalf("child overrides = %#v", child)
|
||||
}
|
||||
if parent.StructuredOutputRepairAttempts == nil || *parent.StructuredOutputRepairAttempts != 2 {
|
||||
t.Fatalf("parent settings changed: %#v", parent)
|
||||
}
|
||||
if len(child.Steps) != len(test.wantStepIDs) {
|
||||
t.Fatalf("child steps = %#v, want %v", child.Steps, test.wantStepIDs)
|
||||
}
|
||||
for i, want := range test.wantStepIDs {
|
||||
if child.Steps[i].ID != want {
|
||||
t.Fatalf("child step %d = %q, want %q", i, child.Steps[i].ID, want)
|
||||
}
|
||||
}
|
||||
if test.childSteps == "" && child.Steps[0].Artifacts["score"].Extract.Module != "extract" {
|
||||
t.Fatalf("inherited first artifact = %#v", child.Steps[0].Artifacts["score"])
|
||||
}
|
||||
|
||||
for _, id := range []string{"parent", "child"} {
|
||||
resolved, err := loaded.Config.Resolve(ResolveInput{PipelineID: id, Catalog: effectiveCatalog(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve(%q) error = %v", id, err)
|
||||
}
|
||||
if resolved.ResolvedPipeline.ID != id {
|
||||
t.Fatalf("resolved pipeline ID = %q, want %q", resolved.ResolvedPipeline.ID, id)
|
||||
}
|
||||
wantResolvedSteps := len(test.wantStepIDs)
|
||||
if id == "parent" {
|
||||
wantResolvedSteps = 2
|
||||
}
|
||||
if len(resolved.ResolvedPipeline.Steps) != wantResolvedSteps {
|
||||
t.Fatalf("resolved %q steps = %#v", id, resolved.ResolvedPipeline.Steps)
|
||||
}
|
||||
if id == "child" && resolved.ResolvedPipeline.Steps[0].ArtifactLanes[0].Extract.LLMProfile != "local" {
|
||||
t.Fatalf("resolved child profile = %q", resolved.ResolvedPipeline.Steps[0].ArtifactLanes[0].Extract.LLMProfile)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactLaneInheritanceUsesSiblingMappingComposition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
loaded, err := loadConfigSource(configkit.Bytes("lanes.yml", []byte(`version: 5
|
||||
pipelines:
|
||||
session:
|
||||
input: {module: input}
|
||||
artifacts:
|
||||
base:
|
||||
extract: {module: extract, retries: 2}
|
||||
derived:
|
||||
extends: base
|
||||
extract: {retries: 4}
|
||||
`)), LoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadConfigSource() error = %v", err)
|
||||
}
|
||||
|
||||
lanes := loaded.Config.Pipelines["session"].Artifacts
|
||||
if lanes["base"].Extract.Module != "extract" || lanes["base"].Extract.Retries != 2 {
|
||||
t.Fatalf("base lane = %#v", lanes["base"])
|
||||
}
|
||||
if lanes["derived"].Extract.Module != "extract" || lanes["derived"].Extract.Retries != 4 {
|
||||
t.Fatalf("derived lane = %#v", lanes["derived"])
|
||||
}
|
||||
if _, err := loaded.Config.Resolve(ResolveInput{PipelineID: "session", Catalog: effectiveCatalog(t)}); err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user