Demonstrate composed configuration comparison
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implement ConfigKit adoption and configuration comparison
|
# Implement ConfigKit adoption and configuration comparison
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–9 are complete.
|
Status: implementation in progress; Stages 1–10 are complete.
|
||||||
|
|
||||||
## Scope and authority
|
## Scope and authority
|
||||||
|
|
||||||
@@ -506,9 +506,18 @@ different selections or loader inputs; PromptKit remains an independent capabili
|
|||||||
supplied to both sides can make their effective defaults equal. Include
|
supplied to both sides can make their effective defaults equal. Include
|
||||||
`--limit=0` when campaign inspection triggers the existing input check.
|
`--limit=0` when campaign inspection triggers the existing input check.
|
||||||
|
|
||||||
**Completion evidence:** meaningful simple and composed diff reports, cache
|
**Completion evidence — complete:** the composed example now loads its two local
|
||||||
separation for different selections, unchanged representative PromptKit/domain
|
files through ConfigKit, applies explicitly supplied Campaign as a native
|
||||||
commands, and a usage failure for unsupported `--scope promptkit` comparison.
|
`default_campaign` override, and caches successful Results by complete supplied
|
||||||
|
selection and Campaign presence/value. Tests confirm the ordinary fixture diff
|
||||||
|
reports `changes.default_campaign: autumn`, a shared `--campaign spring` override
|
||||||
|
produces equal effective values, distinct file and supplied-campaign keys do not
|
||||||
|
reuse the wrong result, equal keys may reuse, nil state bypasses caching, and
|
||||||
|
failed loads are not retained. Representative engine validate/show/sources/
|
||||||
|
explain, named prompt/profile inspection, and session restore commands continue
|
||||||
|
to succeed without model calls. The PromptKit config scope remains unsupported
|
||||||
|
by `config diff`; that selection returns a usage error. README commands and cache
|
||||||
|
identity explanation are updated. `go test -count=1 ./...` passes in ckp.
|
||||||
|
|
||||||
## Stage 11: Reconcile operator, API, and consumer documentation
|
## Stage 11: Reconcile operator, API, and consumer documentation
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ definition inspection, and one application-owned command:
|
|||||||
```sh
|
```sh
|
||||||
go run . config scopes
|
go run . config scopes
|
||||||
go run . config validate --scope all --campaign spring --limit=0
|
go run . config validate --scope all --campaign spring --limit=0
|
||||||
|
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml
|
||||||
|
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --limit=0
|
||||||
go run . promptkit show prompt briefing --campaign spring --format yaml
|
go run . promptkit show prompt briefing --campaign spring --format yaml
|
||||||
go run . promptkit show profile spring --campaign spring
|
go run . promptkit show profile spring --campaign spring
|
||||||
go run . session restore session-42 --campaign spring
|
go run . session restore session-42 --campaign spring
|
||||||
@@ -20,3 +22,12 @@ cache independently for its invocation, using the same application and engine
|
|||||||
loader functions. See the
|
loader functions. See the
|
||||||
[consumer guide](https://gitea.maximumdirect.net/eric/CommandKit/src/branch/main/docs/consumers/guide.md)
|
[consumer guide](https://gitea.maximumdirect.net/eric/CommandKit/src/branch/main/docs/consumers/guide.md)
|
||||||
and the suites' GoDoc for initialization timing and ownership.
|
and the suites' GoDoc for initialization timing and ownership.
|
||||||
|
|
||||||
|
The application-scope comparison loads the local `before.yml` and `after.yml`
|
||||||
|
fixtures and reports `changes.default_campaign: autumn`. The second invocation
|
||||||
|
supplies `spring` to both sides, so the effective defaults compare equal; the
|
||||||
|
application input check also requires `--limit=0` when `--campaign` is supplied.
|
||||||
|
The example's shared run cache keys successful results by the complete supplied
|
||||||
|
configuration/overlay selection and the supplied campaign, including presence.
|
||||||
|
The campaign override is applied through ConfigKit before loading. These are
|
||||||
|
choices for this example's loader and do not define a CommandKit cache policy.
|
||||||
|
|||||||
12
integrations/ckprompt/examples/composed/fixtures/after.yml
Normal file
12
integrations/ckprompt/examples/composed/fixtures/after.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
default_campaign: autumn
|
||||||
|
campaigns:
|
||||||
|
spring:
|
||||||
|
prompt_id: briefing
|
||||||
|
prompt_version: "1"
|
||||||
|
profile_id: spring
|
||||||
|
model: spring-model
|
||||||
|
autumn:
|
||||||
|
prompt_id: briefing
|
||||||
|
prompt_version: "1"
|
||||||
|
profile_id: autumn
|
||||||
|
model: autumn-model
|
||||||
12
integrations/ckprompt/examples/composed/fixtures/before.yml
Normal file
12
integrations/ckprompt/examples/composed/fixtures/before.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
default_campaign: spring
|
||||||
|
campaigns:
|
||||||
|
spring:
|
||||||
|
prompt_id: briefing
|
||||||
|
prompt_version: "1"
|
||||||
|
profile_id: spring
|
||||||
|
model: spring-model
|
||||||
|
autumn:
|
||||||
|
prompt_id: briefing
|
||||||
|
prompt_version: "1"
|
||||||
|
profile_id: autumn
|
||||||
|
model: autumn-model
|
||||||
@@ -31,10 +31,15 @@ type inspectionInput struct {
|
|||||||
Limit commandkit.Supplied[int]
|
Limit commandkit.Supplied[int]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type applicationCacheKey struct {
|
||||||
|
selection ckconfig.Selection
|
||||||
|
campaign commandkit.Supplied[string]
|
||||||
|
}
|
||||||
|
|
||||||
// runCache belongs to one invocation and is shared only where the application
|
// runCache belongs to one invocation and is shared only where the application
|
||||||
// chooses to pass it to its loaders.
|
// chooses to pass it to its loaders.
|
||||||
type runCache struct {
|
type runCache struct {
|
||||||
application *configkit.Result[appConfiguration]
|
applications map[applicationCacheKey]*configkit.Result[appConfiguration]
|
||||||
}
|
}
|
||||||
|
|
||||||
type restoreInput struct {
|
type restoreInput struct {
|
||||||
@@ -53,22 +58,44 @@ func campaignSelector() commandkit.Flag[*inspectionInput] {
|
|||||||
func(input **inspectionInput, value commandkit.Supplied[string]) { (*input).Campaign = value })
|
func(input **inspectionInput, value commandkit.Supplied[string]) { (*input).Campaign = value })
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadApplicationConfig(ctx context.Context, _ ckconfig.Selection, _ *inspectionInput, cache *runCache) (*configkit.Result[appConfiguration], error) {
|
func loadApplicationConfig(ctx context.Context, selection ckconfig.Selection, input *inspectionInput, cache *runCache) (*configkit.Result[appConfiguration], error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if cache != nil && cache.application != nil {
|
campaignInput := commandkit.Supplied[string]{}
|
||||||
return cache.application, nil
|
if input != nil {
|
||||||
|
campaignInput = input.Campaign
|
||||||
}
|
}
|
||||||
result, err := configkit.Load[appConfiguration](configkit.Mapping("composed application settings", map[string]any{
|
key := applicationCacheKey{selection: selection, campaign: campaignInput}
|
||||||
|
if cache != nil {
|
||||||
|
if cached, ok := cache.applications[key]; ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
source := configkit.Mapping("composed application settings", map[string]any{
|
||||||
"default_campaign": "spring",
|
"default_campaign": "spring",
|
||||||
"campaigns": map[string]any{
|
"campaigns": map[string]any{
|
||||||
"spring": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "spring", "model": "spring-model"},
|
"spring": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "spring", "model": "spring-model"},
|
||||||
"autumn": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "autumn", "model": "autumn-model"},
|
"autumn": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "autumn", "model": "autumn-model"},
|
||||||
},
|
},
|
||||||
}))
|
})
|
||||||
if err == nil && cache != nil {
|
if selection.Config.Set {
|
||||||
cache.application = result
|
source = configkit.File(selection.Config.Value)
|
||||||
|
}
|
||||||
|
options := configkit.Options[appConfiguration]{}
|
||||||
|
if selection.Overlay.Set {
|
||||||
|
options.Overlay = selection.Overlay.Value
|
||||||
|
options.Filesystem = &configkit.FilesystemContext{Root: ".", BaseDir: "."}
|
||||||
|
}
|
||||||
|
if campaignInput.Set {
|
||||||
|
options.Overrides = []configkit.Override{{Path: configkit.Keys("default_campaign"), Value: campaignInput.Value, Flag: "--campaign"}}
|
||||||
|
}
|
||||||
|
result, err := configkit.Load(source, options)
|
||||||
|
if err == nil && result != nil && cache != nil {
|
||||||
|
if cache.applications == nil {
|
||||||
|
cache.applications = make(map[applicationCacheKey]*configkit.Result[appConfiguration])
|
||||||
|
}
|
||||||
|
cache.applications[key] = result
|
||||||
}
|
}
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|||||||
160
integrations/ckprompt/examples/composed/main_test.go
Normal file
160
integrations/ckprompt/examples/composed/main_test.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComposedApplicationDiffUsesFixturesAndCampaignOverride(t *testing.T) {
|
||||||
|
app, err := newApp()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
args := []string{"config", "diff", "--scope", "app", "--config", "fixtures/before.yml", "--against-config", "fixtures/after.yml"}
|
||||||
|
var output bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||||
|
t.Fatalf("fixture diff outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
value := composedReportValue(t, output.Bytes(), "changes", "default_campaign")
|
||||||
|
if value == nil || value.Value != "autumn" {
|
||||||
|
t.Fatalf("fixture diff default_campaign = %#v; report %q", value, output.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Reset()
|
||||||
|
args = append(args, "--campaign", "spring", "--limit=0")
|
||||||
|
outcome = app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||||
|
t.Fatalf("overridden fixture diff outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
for _, category := range []string{"additions", "changes", "deletions"} {
|
||||||
|
if node := composedReportValue(t, output.Bytes(), category); len(node.Content) != 0 {
|
||||||
|
t.Fatalf("campaign override left %s: %q", category, output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplicationCacheSeparatesSelectionsCampaignValuesAndPresence(t *testing.T) {
|
||||||
|
cache := &runCache{}
|
||||||
|
beforeSelection := ckconfigSelection("fixtures/before.yml")
|
||||||
|
afterSelection := ckconfigSelection("fixtures/after.yml")
|
||||||
|
load := func(selection ckconfig.Selection, input *inspectionInput) *configkit.Result[appConfiguration] {
|
||||||
|
t.Helper()
|
||||||
|
result, err := loadApplicationConfig(context.Background(), selection, input, cache)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
before := load(beforeSelection, nil)
|
||||||
|
after := load(afterSelection, nil)
|
||||||
|
if before.Config.DefaultCampaign != "spring" || after.Config.DefaultCampaign != "autumn" || before == after {
|
||||||
|
t.Fatalf("file selections shared or returned wrong values: before=%#v after=%#v", before.Config, after.Config)
|
||||||
|
}
|
||||||
|
defaultAfter := load(afterSelection, nil)
|
||||||
|
suppliedSpring := load(afterSelection, &inspectionInput{Campaign: commandkit.Supplied[string]{Value: "spring", Set: true}})
|
||||||
|
suppliedAutumn := load(afterSelection, &inspectionInput{Campaign: commandkit.Supplied[string]{Value: "autumn", Set: true}})
|
||||||
|
suppliedEmpty := load(afterSelection, &inspectionInput{Campaign: commandkit.Supplied[string]{Set: true}})
|
||||||
|
if defaultAfter != after || suppliedSpring.Config.DefaultCampaign != "spring" || suppliedAutumn.Config.DefaultCampaign != "autumn" || suppliedEmpty.Config.DefaultCampaign != "" {
|
||||||
|
t.Fatalf("campaign loading values = default:%q spring:%q autumn:%q empty:%q", defaultAfter.Config.DefaultCampaign, suppliedSpring.Config.DefaultCampaign, suppliedAutumn.Config.DefaultCampaign, suppliedEmpty.Config.DefaultCampaign)
|
||||||
|
}
|
||||||
|
if suppliedSpring == suppliedAutumn || suppliedAutumn == defaultAfter || suppliedEmpty == defaultAfter || len(cache.applications) != 5 {
|
||||||
|
t.Fatalf("cache did not distinguish selection/input presence: entries=%d", len(cache.applications))
|
||||||
|
}
|
||||||
|
|
||||||
|
uncachedOne, err := loadApplicationConfig(context.Background(), afterSelection, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
uncachedTwo, err := loadApplicationConfig(context.Background(), afterSelection, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if uncachedOne == uncachedTwo {
|
||||||
|
t.Fatal("nil cache reused an application result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplicationCacheDoesNotRetainFailedLoads(t *testing.T) {
|
||||||
|
cache := &runCache{}
|
||||||
|
selection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "fixtures/missing.yml", Set: true}}
|
||||||
|
for range 2 {
|
||||||
|
if _, err := loadApplicationConfig(context.Background(), selection, nil, cache); err == nil {
|
||||||
|
t.Fatal("missing configuration unexpectedly loaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cache.applications) != 0 {
|
||||||
|
t.Fatalf("failed result was cached: %#v", cache.applications)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposedPromptKitAndDomainCommandsRemainAvailable(t *testing.T) {
|
||||||
|
app, err := newApp()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
commands := [][]string{
|
||||||
|
{"config", "validate", "--scope", "all", "--campaign", "spring", "--limit=0"},
|
||||||
|
{"config", "show", "--scope", "promptkit", "--campaign", "spring"},
|
||||||
|
{"config", "sources", "--scope", "promptkit", "--campaign", "spring"},
|
||||||
|
{"config", "explain", "--scope", "promptkit", "--path", "/profiles/spring/model", "--campaign", "spring"},
|
||||||
|
{"promptkit", "show", "prompt", "briefing", "--campaign", "spring", "--format", "yaml"},
|
||||||
|
{"promptkit", "show", "profile", "spring", "--campaign", "spring"},
|
||||||
|
{"session", "restore", "session-42", "--campaign", "spring"},
|
||||||
|
}
|
||||||
|
for _, args := range commands {
|
||||||
|
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||||
|
t.Errorf("command %q outcome = %#v", args, outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPromptKitScopeDoesNotSupportConfigurationDiff(t *testing.T) {
|
||||||
|
app, err := newApp()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeUsage || output.Len() != 0 {
|
||||||
|
t.Fatalf("PromptKit diff outcome/output = %#v / %q", outcome, output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func composedReportValue(t *testing.T, data []byte, category string, fields ...string) *yaml.Node {
|
||||||
|
t.Helper()
|
||||||
|
var document yaml.Node
|
||||||
|
if err := yaml.Unmarshal(data, &document); err != nil {
|
||||||
|
t.Fatalf("decode composed report %q: %v", data, err)
|
||||||
|
}
|
||||||
|
node := composedField(document.Content[0], category)
|
||||||
|
for _, field := range fields {
|
||||||
|
node = composedField(node, field)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func composedField(mapping *yaml.Node, name string) *yaml.Node {
|
||||||
|
if mapping == nil || mapping.Kind != yaml.MappingNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for index := 0; index+1 < len(mapping.Content); index += 2 {
|
||||||
|
if mapping.Content[index].Value == name {
|
||||||
|
return mapping.Content[index+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ckconfigSelection(path string) ckconfig.Selection {
|
||||||
|
return ckconfig.Selection{Config: commandkit.Supplied[string]{Value: path, Set: true}}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user