From d40a01053874d0b669cf3e0a97bfbd39cfb820f5 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 17 Oct 2026 05:41:21 +0000 Subject: [PATCH] Store one prompt definition per ID --- assembled_workflow_benchmark_test.go | 5 +- capacity_contract_test.go | 2 +- configuration_inspection_contract_test.go | 2 +- construction_contract_test.go | 45 ++---- consumer_test_helpers_test.go | 5 +- docs/formats.md | 73 +++++---- docs/roadmap/implementation.md | 7 + .../observe/prompts/assessment.yaml | 32 ++-- .../go-library/observe/prompts/proposal.yaml | 32 ++-- examples/go-library/prepare/prompt.yaml | 30 ++-- examples/go-library/run/prompt.yaml | 30 ++-- frozen_rendering_contract_test.go | 23 +-- internal/configuration/benchmark_test.go | 13 +- internal/configuration/listing_test.go | 17 +-- internal/configuration/load.go | 43 +++--- internal/configuration/load_test.go | 7 +- internal/configuration/prompts.go | 140 ++++++++---------- internal/configuration/prompts_test.go | 129 +++++++--------- internal/usecase/smoke_selection_test.go | 24 ++- listing_contract_test.go | 28 ++-- observation_boundary_contract_test.go | 5 +- prepared_execution_contract_test.go | 31 ++-- prepared_observation_contract_test.go | 5 +- profile_inheritance_contract_test.go | 4 +- profile_inspection_contract_test.go | 5 +- reliable_execution_contract_test.go | 37 ++--- requirements_inspection_contract_test.go | 59 ++++---- smoke_integration_contract_test.go | 5 +- 28 files changed, 393 insertions(+), 445 deletions(-) diff --git a/assembled_workflow_benchmark_test.go b/assembled_workflow_benchmark_test.go index ade3b79..b54c963 100644 --- a/assembled_workflow_benchmark_test.go +++ b/assembled_workflow_benchmark_test.go @@ -100,11 +100,12 @@ func newAssembledWorkflowFixture(b *testing.B, name string) assembledWorkflowFix config := map[string]any{ "profiles": map[string]any{"profile": profile}, - "prompts": map[string]any{"workflow": map[string]any{"v1": map[string]any{ + "prompts": map[string]any{"workflow": map[string]any{ + "version": "v1", "default_profile": "profile", "messages": []any{map[string]any{"role": "user", "content": message}}, "output": output, - }}}, + }}, } engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{ Source: configkit.Mapping("assembled workflow benchmark", config), diff --git a/capacity_contract_test.go b/capacity_contract_test.go index 976e909..d3202ca 100644 --- a/capacity_contract_test.go +++ b/capacity_contract_test.go @@ -267,7 +267,7 @@ func newBackendCapacityEngine( Source: configkit.Mapping("capacity fixture", map[string]any{ "backends": map[string]any{"limited": backend}, "profiles": map[string]any{"profile": map[string]any{"backend": "limited", "model": "model"}}, - "prompts": map[string]any{promptID: map[string]any{"1": definition}}, + "prompts": map[string]any{promptID: promptDefinitionWithVersion("1", definition)}, }), }}}, options...) if err != nil { diff --git a/configuration_inspection_contract_test.go b/configuration_inspection_contract_test.go index 41e6761..97bd1dd 100644 --- a/configuration_inspection_contract_test.go +++ b/configuration_inspection_contract_test.go @@ -199,7 +199,7 @@ func TestConfigurationInspectionUsesCapturedSourcesWithoutProviderCalls(t *testi t.Fatalf("captured difference changed between reads: first=%+v next=%+v", expected, differences[0]) } } - if _, found := view.Explain(configkit.Keys("prompts", "captured", "1", "messages").Index(0).Key("content_file")); !found { + if _, found := view.Explain(configkit.Keys("prompts", "captured", "messages").Index(0).Key("content_file")); !found { t.Fatal("captured prompt content path was not inspectable") } var report bytes.Buffer diff --git a/construction_contract_test.go b/construction_contract_test.go index 8438010..f0895d7 100644 --- a/construction_contract_test.go +++ b/construction_contract_test.go @@ -73,11 +73,12 @@ func TestProfileEnvironmentLookupAcceptsNonShellNames(t *testing.T) { func TestPublicConstructionWithLocalBackendAndProgrammaticProfile(t *testing.T) { backendID, model := "workstation", "offline-model" prompt := configkit.Input{Source: configkit.Mapping("prompt", map[string]any{ - "prompts": map[string]any{"hello": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"hello": map[string]any{ + "version": "1", "default_profile": "offline", "messages": []any{map[string]any{"role": "user", "content": "Hello"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}, + }}, })} client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "reply"}} engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ @@ -115,14 +116,14 @@ profiles: model: default-model prompts: hello: - "1": - default_profile: application - messages: - - role: user - content: Hello - output: - format: text - validation_mode: none + version: "1" + default_profile: application + messages: + - role: user + content: Hello + output: + format: text + validation_mode: none `)}, }, "config") if err != nil { @@ -269,24 +270,10 @@ func TestBackendSourceOwnsSmokeTestValuesAndDefersDefaultLookup(t *testing.T) { } } -func TestPreparationRejectsAmbiguousPromptVersion(t *testing.T) { - definition := func(content string) map[string]any { - return map[string]any{ - "messages": []any{map[string]any{"role": "user", "content": content}}, - "output": map[string]any{"format": "text", "validation_mode": "none"}, - } - } - engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ - Main: &configkit.Input{Source: configkit.Mapping("versions", map[string]any{ - "prompts": map[string]any{"hello": map[string]any{ - "1": definition("first"), "2": definition("second"), - }}, - })}, - }}) - if err != nil { - t.Fatalf("construct engine: %v", err) - } - if result, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "hello"}); result != nil || !errors.Is(err, promptkit.ErrPromptLoad) { - t.Fatalf("ambiguous preparation = (%#v, %v), want ErrPromptLoad", result, err) +func TestPreparationSelectsSinglePromptDefinitionByID(t *testing.T) { + engine := newSimpleEngine(t, "selected") + prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"}) + if err != nil || prepared == nil || len(prepared.Messages) != 1 || prepared.Messages[0].Content != "selected" { + t.Fatalf("prepare prompt by ID = (%+v, %v)", prepared, err) } } diff --git a/consumer_test_helpers_test.go b/consumer_test_helpers_test.go index 0597853..3bf4dc1 100644 --- a/consumer_test_helpers_test.go +++ b/consumer_test_helpers_test.go @@ -63,11 +63,12 @@ func simpleConfig(content string) promptkit.Config { "profiles": map[string]any{"profile": map[string]any{ "endpoint": "http://example.test/v1", "model": "model", }}, - "prompts": map[string]any{"prompt": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"prompt": map[string]any{ + "version": "1", "default_profile": "profile", "messages": []any{map[string]any{"role": "user", "content": content}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}, + }}, })}, }} } diff --git a/docs/formats.md b/docs/formats.md index 5d5c813..344d743 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -12,10 +12,10 @@ This reference describes what Promptkit does with the final effective values. A configuration document has keyed top-level `backends`, `profiles`, and `prompts` mappings. Their keys are literal IDs, not filename-derived identities. -Prompt versions are a second keyed mapping. Keys and references must be -nonblank and remain exact: Promptkit does not trim or re-key them. Old embedded -`id`, `version`, and `base_profile` fields are invalid. Unknown fields are -errors. +Each prompt ID maps directly to one definition; an optional `version` string is +descriptive metadata, not a selection key. Keys and references must be nonblank +and remain exact: Promptkit does not trim or re-key them. Old embedded `id` and +`base_profile` fields are invalid. Unknown fields are errors. ```yaml backends: @@ -28,14 +28,14 @@ profiles: model: example-model prompts: meeting.summary: - "1.0.0": - default_profile: local-summary - messages: - - role: user - content: Summarize this note. - output: - format: text - validation_mode: none + version: "1.0.0" + default_profile: local-summary + messages: + - role: user + content: Summarize this note. + output: + format: text + validation_mode: none ``` Every effective backend, profile, and prompt definition is validated when @@ -56,32 +56,32 @@ see the consumer guide's [profile and prompt listing](consumers/pkg-promptkit.md ## Prompt Definitions -The `prompts` mapping selects a literal prompt ID, then a string version key: +The `prompts` mapping selects a literal prompt ID whose value is the definition: ```yaml prompts: meeting.summary: - "1.0.0": - default_profile: local-summary - description: Summarize a synthetic meeting note. - session_id: '{{.session}}' - inputs: - - name: note - required: true - content_type: text/plain - description: Meeting note to summarize. - messages: - - role: system - content: Return a concise summary. - cache_control: - type: ephemeral - ttl: 1h - - role: user - content_file: ./summary.user.md - output: - format: markdown - validation_mode: basic - repair_attempts: 0 + version: "1.0.0" + default_profile: local-summary + description: Summarize a synthetic meeting note. + session_id: '{{.session}}' + inputs: + - name: note + required: true + content_type: text/plain + description: Meeting note to summarize. + messages: + - role: system + content: Return a concise summary. + cache_control: + type: ephemeral + ttl: 1h + - role: user + content_file: ./summary.user.md + output: + format: markdown + validation_mode: basic + repair_attempts: 0 ``` `messages` must be nonempty and `output` must be a valid mapping. Optional @@ -89,9 +89,8 @@ prompts: fields may be absent or cleared with null where appropriate. A null whole definition, required role, output format, or validation mode is invalid. `required` and `repair_attempts` default to false and zero, respectively, and -are not nullable. A request that omits a prompt version succeeds only if that -prompt ID has exactly one effective version. `InspectPrompt` selects from the -same captured definitions without rendering or resolving a profile. +are not nullable. `InspectPrompt` selects the same captured definition without +rendering or resolving a profile. ### Inputs diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 15a5cb0..4d1361c 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -390,6 +390,13 @@ Validation: `GOWORK=off go test ./...`, including consumers of migrated fixtures Test ID-only selection, source-relative content, and inherited/overlaid prompt settings. Do not add tests preserving temporary obsolete selector parameters. +Completion: prompts now map directly from each ID to one definition, with an +optional descriptive version label. Configuration lookup and message/input +metadata paths are ID-only; ambiguity handling and version-keyed indexes are +removed. Fixtures, examples, and the canonical format reference use the direct +shape, including layered overlays. Validation passed: `GOWORK=off go test ./...`, +`GOWORK=off python3 scripts/check-examples.py`, and `git diff --check`. + ## Stage 5: Remove Request-Side Version Selection Scope: compile-directed removal of obsolete selector plumbing; no new lookup rules. diff --git a/examples/go-library/observe/prompts/assessment.yaml b/examples/go-library/observe/prompts/assessment.yaml index 66550d4..1bb75d7 100644 --- a/examples/go-library/observe/prompts/assessment.yaml +++ b/examples/go-library/observe/prompts/assessment.yaml @@ -1,18 +1,18 @@ prompts: example.assessment: - "1.0.0": - default_profile: assessment-profile - description: Assess a structured proposal independently of schema validity. - inputs: - - name: proposal - required: true - content_type: application/json - messages: - - role: system - content: Return a concise assessment as JSON. - - role: user - content: '{{input "proposal"}}' - output: - format: json - validation_mode: json_schema - schema_path: assessment.schema.json + version: "1.0.0" + default_profile: assessment-profile + description: Assess a structured proposal independently of schema validity. + inputs: + - name: proposal + required: true + content_type: application/json + messages: + - role: system + content: Return a concise assessment as JSON. + - role: user + content: '{{input "proposal"}}' + output: + format: json + validation_mode: json_schema + schema_path: assessment.schema.json diff --git a/examples/go-library/observe/prompts/proposal.yaml b/examples/go-library/observe/prompts/proposal.yaml index 5729f21..2ba2ea9 100644 --- a/examples/go-library/observe/prompts/proposal.yaml +++ b/examples/go-library/observe/prompts/proposal.yaml @@ -1,18 +1,18 @@ prompts: example.proposal: - "1.0.0": - default_profile: proposal-profile - description: Propose a bounded improvement from a synthetic idea. - inputs: - - name: idea - required: true - content_type: text/plain - messages: - - role: system - content: Return a concise proposal as JSON. - - role: user - content: '{{input "idea"}}' - output: - format: json - validation_mode: json_schema - schema_path: proposal.schema.json + version: "1.0.0" + default_profile: proposal-profile + description: Propose a bounded improvement from a synthetic idea. + inputs: + - name: idea + required: true + content_type: text/plain + messages: + - role: system + content: Return a concise proposal as JSON. + - role: user + content: '{{input "idea"}}' + output: + format: json + validation_mode: json_schema + schema_path: proposal.schema.json diff --git a/examples/go-library/prepare/prompt.yaml b/examples/go-library/prepare/prompt.yaml index 39dd387..0dea6f4 100644 --- a/examples/go-library/prepare/prompt.yaml +++ b/examples/go-library/prepare/prompt.yaml @@ -1,17 +1,17 @@ prompts: example.prepare: - "1.0.0": - default_profile: offline-example - description: Prepare a prompt without contacting a model provider. - inputs: - - name: note - required: true - content_type: text/plain - messages: - - role: system - content: Summarize the note in one sentence. - - role: user - content: '{{input "note"}}' - output: - format: text - validation_mode: basic + version: "1.0.0" + default_profile: offline-example + description: Prepare a prompt without contacting a model provider. + inputs: + - name: note + required: true + content_type: text/plain + messages: + - role: system + content: Summarize the note in one sentence. + - role: user + content: '{{input "note"}}' + output: + format: text + validation_mode: basic diff --git a/examples/go-library/run/prompt.yaml b/examples/go-library/run/prompt.yaml index 63055a9..6c5f679 100644 --- a/examples/go-library/run/prompt.yaml +++ b/examples/go-library/run/prompt.yaml @@ -1,17 +1,17 @@ prompts: example.run: - "1.0.0": - default_profile: offline-example - description: Run a prompt with a deterministic injected model client. - inputs: - - name: note - required: true - content_type: text/plain - messages: - - role: system - content: Summarize the note in one sentence. - - role: user - content: '{{input "note"}}' - output: - format: text - validation_mode: basic + version: "1.0.0" + default_profile: offline-example + description: Run a prompt with a deterministic injected model client. + inputs: + - name: note + required: true + content_type: text/plain + messages: + - role: system + content: Summarize the note in one sentence. + - role: user + content: '{{input "note"}}' + output: + format: text + validation_mode: basic diff --git a/frozen_rendering_contract_test.go b/frozen_rendering_contract_test.go index 63ec6ae..71b0bec 100644 --- a/frozen_rendering_contract_test.go +++ b/frozen_rendering_contract_test.go @@ -19,15 +19,15 @@ import ( const capturedPromptDocument = ` prompts: captured: - "1": - default_profile: offline - session_id: '{{.session}}' - messages: - - role: user - content_file: body.tmpl - output: - format: text - validation_mode: none + version: "1" + default_profile: offline + session_id: '{{.session}}' + messages: + - role: user + content_file: body.tmpl + output: + format: text + validation_mode: none ` func capturedPromptEngine(t *testing.T, source fs.FS) *promptkit.Engine { @@ -149,12 +149,13 @@ func TestPreparationStillReadsRequestArtifactsAndSchemasAfterConstruction(t *tes "offline": {Endpoint: &endpoint, Model: &model}, })}}}}, Main: &configkit.Input{Source: configkit.Mapping("structured prompt", map[string]any{ - "prompts": map[string]any{"structured": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"structured": map[string]any{ + "version": "1", "default_profile": "offline", "inputs": []any{map[string]any{"name": "note", "required": true}}, "messages": []any{map[string]any{"role": "user", "content": `{{input "note"}}`}}, "output": map[string]any{"format": "json", "validation_mode": "json_schema", "schema_path": "schema.json"}, - }}}, + }}, })}, }}, promptkit.WithSchemaFS(schemas, ".")) if err != nil { diff --git a/internal/configuration/benchmark_test.go b/internal/configuration/benchmark_test.go index 370a907..71f193a 100644 --- a/internal/configuration/benchmark_test.go +++ b/internal/configuration/benchmark_test.go @@ -31,6 +31,7 @@ type configurationShape struct { func benchmarkPrompt(profileID string) map[string]any { return map[string]any{ + "version": "1", "default_profile": profileID, "messages": []any{map[string]any{"role": "user", "content": "Hello {{.name}}"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, @@ -44,7 +45,7 @@ func independentDefinitions(count int) configurationShape { profileID := fmt.Sprintf("profile-%03d", index) promptID := fmt.Sprintf("prompt-%03d", index) profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID} - prompts[promptID] = map[string]any{"1": benchmarkPrompt(profileID)} + prompts[promptID] = benchmarkPrompt(profileID) } return configurationShape{ main: configkit.Input{Source: configkit.Mapping("independent definitions", map[string]any{"profiles": profiles, "prompts": prompts})}, @@ -63,7 +64,7 @@ func fileBackedDefinitions(count int) configurationShape { profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID} prompt := benchmarkPrompt(profileID) prompt["messages"] = []any{map[string]any{"role": "user", "content_file": "body.md"}} - prompts[promptID] = map[string]any{"1": prompt} + prompts[promptID] = prompt } namespace := configkit.NewFilesystem("benchmark content", fstest.MapFS{ "assets/body.md": &fstest.MapFile{Data: []byte("Hello {{.name}}")}, @@ -89,7 +90,7 @@ func sharedBaseDefinitions(count int) configurationShape { return configurationShape{ main: configkit.Input{Source: configkit.Mapping("inherited definitions", map[string]any{ "profiles": profiles, - "prompts": map[string]any{"shared": map[string]any{"1": benchmarkPrompt(fmt.Sprintf("child-%03d", count-1))}}, + "prompts": map[string]any{"shared": benchmarkPrompt(fmt.Sprintf("child-%03d", count-1))}, })}, inputEntries: count + 2, effectiveEntries: count + 2, @@ -117,7 +118,7 @@ func layeredDefinitions(count int) configurationShape { }, main: configkit.Input{Source: configkit.Mapping("main", map[string]any{ "profiles": final, - "prompts": map[string]any{"layered": map[string]any{"1": benchmarkPrompt(fmt.Sprintf("profile-%03d", count-1))}}, + "prompts": map[string]any{"layered": benchmarkPrompt(fmt.Sprintf("profile-%03d", count-1))}, })}, inputEntries: count*4 + 1, effectiveEntries: count + 1, @@ -160,7 +161,7 @@ func BenchmarkConfigurationSelection(b *testing.B) { b.Run("prompt", func(b *testing.B) { b.ReportAllocs() for b.Loop() { - definition, _, err := result.prompt("prompt-127", "1") + definition, _, err := result.prompt("prompt-127") if err != nil { b.Fatal(err) } @@ -201,7 +202,7 @@ func inspectionDefinitions(extraParams map[string]any) configurationShape { prompt := benchmarkPrompt("small") prompt["inputs"] = []any{map[string]any{"name": "transcript", "required": true}} prompts := map[string]any{ - "inspection": map[string]any{"1": prompt}, + "inspection": prompt, } return configurationShape{ main: configkit.Input{Source: configkit.Mapping("inspection definitions", map[string]any{ diff --git a/internal/configuration/listing_test.go b/internal/configuration/listing_test.go index dbd07af..8cb4851 100644 --- a/internal/configuration/listing_test.go +++ b/internal/configuration/listing_test.go @@ -22,12 +22,9 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) { " z/profile ": map[string]any{"backend": "local", "model": "spaced-model"}, }, "prompts": map[string]any{ - "z/prompt": map[string]any{"v1": listingPromptDefinition()}, - "a/prompt": map[string]any{"v1": listingPromptDefinition()}, - " z/prompt ": map[string]any{ - "2": listingPromptDefinition(), - "10": listingPromptDefinition(), - }, + "z/prompt": listingPromptDefinition(), + "a/prompt": listingPromptDefinition(), + " z/prompt ": listingPromptDefinition(), }, }), nil) if err != nil { @@ -48,8 +45,7 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) { t.Fatal(err) } wantPrompts := []domain.PromptIdentity{ - {ID: " z/prompt ", Version: "10"}, - {ID: " z/prompt ", Version: "2"}, + {ID: " z/prompt ", Version: "v1"}, {ID: "a/prompt", Version: "v1"}, {ID: "z/prompt", Version: "v1"}, } @@ -61,7 +57,7 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) { func TestLoadedListsAreIndependent(t *testing.T) { state, err := Load(mapping("identities", map[string]any{ "profiles": map[string]any{"profile": map[string]any{}}, - "prompts": map[string]any{"example": map[string]any{"v1": listingPromptDefinition()}}, + "prompts": map[string]any{"example": listingPromptDefinition()}, }), nil) if err != nil { t.Fatal(err) @@ -116,7 +112,7 @@ func TestLoadedListsEmptyInventory(t *testing.T) { func TestLoadedListsHonorCanceledContext(t *testing.T) { state, err := Load(mapping("identities", map[string]any{ "profiles": map[string]any{"partial": map[string]any{}}, - "prompts": map[string]any{"example": map[string]any{"v1": listingPromptDefinition()}}, + "prompts": map[string]any{"example": listingPromptDefinition()}, }), nil) if err != nil { t.Fatal(err) @@ -136,6 +132,7 @@ func TestLoadedListsHonorCanceledContext(t *testing.T) { func listingPromptDefinition() map[string]any { return map[string]any{ + "version": "v1", "messages": []any{map[string]any{"role": "user", "content": "Hello."}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, } diff --git a/internal/configuration/load.go b/internal/configuration/load.go index 755afad..f6c2cf4 100644 --- a/internal/configuration/load.go +++ b/internal/configuration/load.go @@ -17,9 +17,9 @@ import ( ) type document struct { - Backends map[string]backendConfig `yaml:"backends"` - Profiles map[string]profileConfig `yaml:"profiles"` - Prompts map[string]map[string]promptConfig `yaml:"prompts"` + Backends map[string]backendConfig `yaml:"backends"` + Profiles map[string]profileConfig `yaml:"profiles"` + Prompts map[string]promptConfig `yaml:"prompts"` } type backendConfig struct { @@ -62,6 +62,7 @@ type profileConfig struct { } type promptConfig struct { + Version *string `yaml:"version"` DefaultProfile *string `yaml:"default_profile"` Description *string `yaml:"description"` SessionID *string `yaml:"session_id"` @@ -110,9 +111,8 @@ type loaded struct { result *configkit.Result[document] backends map[string]map[string]fieldState profiles map[string]map[string]fieldState - promptMetadata map[string]map[string][]promptInputPresence - prompts map[string]map[string]*promptEntry - ambiguous map[string]bool + promptMetadata map[string][]promptInputPresence + prompts map[string]*promptEntry registry *backend.Registry indexedProfiles map[string]domain.ExecutionProfile profileIDs []string @@ -149,7 +149,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) state := &loaded{ backends: make(map[string]map[string]fieldState), profiles: make(map[string]map[string]fieldState), - promptMetadata: make(map[string]map[string][]promptInputPresence), + promptMetadata: make(map[string][]promptInputPresence), } result, err := configkit.LoadLayered(main, configkit.LayeredOptions[document]{ Underlays: underlays, @@ -181,9 +181,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) } state.profileIDs = sortedKeys(state.indexedProfiles) for _, id := range sortedKeys(state.prompts) { - for _, version := range sortedKeys(state.prompts[id]) { - state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: version}) - } + state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: state.prompts[id].definition.Version}) } return state, nil } @@ -193,24 +191,19 @@ type promptInputPresence struct { description fieldState } -func capturePromptMetadata(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) { +func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string][]promptInputPresence) { for _, id := range sortedKeys(prompts) { - if destination[id] == nil { - destination[id] = make(map[string][]promptInputPresence, len(prompts[id])) - } - for _, version := range sortedKeys(prompts[id]) { - inputs := prompts[id][version].Inputs - states := make([]promptInputPresence, len(inputs)) - base := basePath.Key(id).Key(version).Key("inputs") - for index := range inputs { - path := base.Index(index) - states[index] = promptInputPresence{ - contentType: captureFieldState(path.Key("content_type"), resolution), - description: captureFieldState(path.Key("description"), resolution), - } + inputs := prompts[id].Inputs + states := make([]promptInputPresence, len(inputs)) + base := basePath.Key(id).Key("inputs") + for index := range inputs { + path := base.Index(index) + states[index] = promptInputPresence{ + contentType: captureFieldState(path.Key("content_type"), resolution), + description: captureFieldState(path.Key("description"), resolution), } - destination[id][version] = states } + destination[id] = states } } diff --git a/internal/configuration/load_test.go b/internal/configuration/load_test.go index 823b952..1cfcb8d 100644 --- a/internal/configuration/load_test.go +++ b/internal/configuration/load_test.go @@ -21,10 +21,11 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) { "extra_params": map[string]any{"provider": map[string]any{"first": 1, "second": 2}}}, "child.v1": map[string]any{"extends": "base/one"}, }, - "prompts": map[string]any{"invoice/summary": map[string]any{"v1.0": map[string]any{ + "prompts": map[string]any{"invoice/summary": map[string]any{ + "version": "v1.0", "messages": []any{map[string]any{"role": "user", "content": "Summarize."}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, - }}}, + }}, }) overlay := mapping("operator", map[string]any{ "backends": map[string]any{"local.v1": map[string]any{"queue_capacity": 0}}, @@ -54,7 +55,7 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) { if backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || loaded.backends["local.v1"]["queue_capacity"] != fieldValue { t.Fatalf("explicit zero queue capacity was lost: %+v", backend) } - if loaded.result.Config.Prompts["invoice/summary"]["v1.0"].Messages[0].Content == nil { + if loaded.result.Config.Prompts["invoice/summary"].Messages[0].Content == nil || loaded.result.Config.Prompts["invoice/summary"].Version == nil || *loaded.result.Config.Prompts["invoice/summary"].Version != "v1.0" { t.Fatal("literal prompt ID and version were not decoded") } } diff --git a/internal/configuration/prompts.go b/internal/configuration/prompts.go index 6b08461..ef190c9 100644 --- a/internal/configuration/prompts.go +++ b/internal/configuration/prompts.go @@ -16,8 +16,7 @@ import ( ) var ( - errPromptNotFound = errors.New("prompt definition not found") - errPromptAmbiguous = errors.New("prompt version is ambiguous") + errPromptNotFound = errors.New("prompt definition not found") ) type promptEntry struct { @@ -29,64 +28,61 @@ type promptEntry struct { messages []*template.Template } -func resolvePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) { +func resolvePrompts(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) { for _, id := range sortedKeys(prompts) { - for _, version := range sortedKeys(prompts[id]) { - definition := prompts[id][version] - base := basePath.Key(id).Key(version) + definition := prompts[id] + base := basePath.Key(id) + for _, field := range []struct { + name string + value **string + }{ + {"description", &definition.Description}, + {"session_id", &definition.SessionID}, + } { + if *field.value != nil { + normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value)) + } + } + inputsChanged := false + for index := range definition.Inputs { + input := &definition.Inputs[index] + inputPath := base.Key("inputs").Index(index) + normalizedName := strings.TrimSpace(input.Name) + if normalizedName != input.Name { + configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata") + inputsChanged = true + } for _, field := range []struct { name string value **string }{ - {"description", &definition.Description}, - {"session_id", &definition.SessionID}, + {"content_type", &input.ContentType}, + {"description", &input.Description}, } { if *field.value != nil { - normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value)) - } - } - inputsChanged := false - for index := range definition.Inputs { - input := &definition.Inputs[index] - inputPath := base.Key("inputs").Index(index) - normalizedName := strings.TrimSpace(input.Name) - if normalizedName != input.Name { - configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata") - inputsChanged = true - } - for _, field := range []struct { - name string - value **string - }{ - {"content_type", &input.ContentType}, - {"description", &input.Description}, - } { - if *field.value != nil { - normalized := strings.TrimSpace(**field.value) - if normalized != **field.value { - configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata") - inputsChanged = true - } + normalized := strings.TrimSpace(**field.value) + if normalized != **field.value { + configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata") + inputsChanged = true } } } - if inputsChanged { - resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input metadata") - } - // Normalize message metadata on execution-owned copies below. ConfigKit - // records sequence mutations as whole-value replacements, which would - // discard the effective source contexts of unchanged content_file fields. - if definition.Output != nil && definition.Output.SchemaPath != nil { - normalizeString(&definition.Output.SchemaPath, base.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*definition.Output.SchemaPath)) - } - prompts[id][version] = definition } + if inputsChanged { + resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input metadata") + } + // Normalize message metadata on execution-owned copies below. ConfigKit + // records sequence mutations as whole-value replacements, which would + // discard the effective source contexts of unchanged content_file fields. + if definition.Output != nil && definition.Output.SchemaPath != nil { + normalizeString(&definition.Output.SchemaPath, base.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*definition.Output.SchemaPath)) + } + prompts[id] = definition } } -func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error { - state.prompts = make(map[string]map[string]*promptEntry, len(prompts)) - state.ambiguous = make(map[string]bool) +func (state *loaded) materializePrompts(prompts map[string]promptConfig, basePath configkit.Path) error { + state.prompts = make(map[string]*promptEntry, len(prompts)) var metadata *configkit.Metadata lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) { if metadata == nil { @@ -100,23 +96,15 @@ func (state *loaded) materializePrompts(prompts map[string]map[string]promptConf if strings.TrimSpace(id) == "" { return fmt.Errorf("%s: prompt ID must not be blank", promptBase) } - versions := prompts[id] - if versions == nil { - return fmt.Errorf("%s: prompt collection must be a mapping", promptBase) + version := "" + if prompts[id].Version != nil { + version = *prompts[id].Version } - state.prompts[id] = make(map[string]*promptEntry, len(versions)) - state.ambiguous[id] = len(versions) > 1 - for _, version := range sortedKeys(versions) { - versionBase := promptBase.Key(version) - if strings.TrimSpace(version) == "" { - return fmt.Errorf("%s: prompt version must not be blank", versionBase) - } - entry, err := state.materializePrompt(id, version, versions[version], versionBase, lookupOrigin) - if err != nil { - return err - } - state.prompts[id][version] = entry + entry, err := state.materializePrompt(id, version, prompts[id], promptBase, lookupOrigin) + if err != nil { + return err } + state.prompts[id] = entry } return nil } @@ -164,7 +152,7 @@ func (state *loaded) materializePrompt(id, version string, raw promptConfig, bas definition.Inputs = append(definition.Inputs, domain.PromptInput{ Name: input.Name, Required: input.Required, ContentType: contentType, Description: description, }) - presence := state.promptMetadata[id][version][index] + presence := state.promptMetadata[id][index] metadata := prompt.InputMetadata{Name: input.Name, Required: input.Required} if presence.contentType == fieldValue { metadata.ContentType = stringPointer(contentType) @@ -299,19 +287,10 @@ func (state *loaded) readContent(fieldPath configkit.Path, name string, lookupOr } func (state *loaded) lookupPromptEntry(id, version string) (*promptEntry, error) { - if state == nil || strings.TrimSpace(id) == "" || (version != "" && strings.TrimSpace(version) == "") { + if state == nil || strings.TrimSpace(id) == "" { return nil, errPromptNotFound } - versions := state.prompts[id] - if version == "" { - if state.ambiguous[id] { - return nil, errPromptAmbiguous - } - for selected := range versions { - version = selected - } - } - entry := versions[version] + entry := state.prompts[id] if entry == nil { return nil, errPromptNotFound } @@ -342,8 +321,8 @@ func promptLookupError(err error) error { return err } -func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domain.PromptInspection, error) { - entry, err := state.lookupPromptEntry(id, version) +func (state *loaded) prompt(id string) (*domain.PromptDefinition, domain.PromptInspection, error) { + entry, err := state.lookupPromptEntry(id, "") if err != nil { return nil, domain.PromptInspection{}, err } @@ -351,7 +330,8 @@ func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domai } // GetPromptSnapshot returns an independently owned captured definition and its -// construction-time hash. Lookup retains literal ID and version keys. +// construction-time hash. The version argument is retained temporarily at the +// adapter boundary and does not participate in lookup. func (state *loaded) GetPromptSnapshot(ctx context.Context, id, version string) (*domain.PromptDefinition, string, error) { if err := ctx.Err(); err != nil { return nil, "", err @@ -382,12 +362,12 @@ func (state *loaded) GetPromptDefinition(ctx context.Context, id, version string return definition, err } -// GetCompiledTemplates returns construction-time parses for one exact prompt. +// GetCompiledTemplates returns construction-time parses for one prompt ID. func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTemplates, bool) { if state == nil { return prompt.CompiledTemplates{}, false } - entry := state.prompts[id][version] + entry := state.prompts[id] if entry == nil { return prompt.CompiledTemplates{}, false } @@ -395,13 +375,13 @@ func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTe } // GetRequirementSnapshot returns captured templates and input metadata for -// one exact prompt identity. Returned metadata and slices are independently +// one prompt ID. Returned metadata and slices are independently // owned; the compiled templates are shared immutable construction-time parses. func (state *loaded) GetRequirementSnapshot(id, version string) (prompt.RequirementSnapshot, bool) { if state == nil { return prompt.RequirementSnapshot{}, false } - entry := state.prompts[id][version] + entry := state.prompts[id] if entry == nil { return prompt.RequirementSnapshot{}, false } diff --git a/internal/configuration/prompts_test.go b/internal/configuration/prompts_test.go index c3d00d2..0cd5a64 100644 --- a/internal/configuration/prompts_test.go +++ b/internal/configuration/prompts_test.go @@ -17,7 +17,7 @@ import ( func TestLoadCapturesInheritedImportedContent(t *testing.T) { files := fstest.MapFS{ "app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\nprompts:\n child:\n extends: base\n")}, - "catalog/base.yaml": {Data: []byte("prompts:\n base:\n v1:\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")}, + "catalog/base.yaml": {Data: []byte("prompts:\n base:\n version: \"v1\"\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")}, "catalog/body.md": {Data: []byte("Captured {{.name}}")}, } namespace := configkit.NewFilesystem("embedded", files) @@ -32,7 +32,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) { } t.Fatal(err) } - definition, inspection, err := state.prompt("child", "") + definition, inspection, err := state.prompt("child") if err != nil { t.Fatal(err) } @@ -43,7 +43,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) { if message.Role != "user" || message.CacheControl == nil || message.CacheControl.Type != "ephemeral" || message.CacheControl.TTL != "1h" { t.Fatalf("message metadata was not normalized: %+v", message) } - field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "v1", "messages").Index(0).Key("content_file")) + field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "messages").Index(0).Key("content_file")) if !ok || len(field.Origin.Locations) != 1 { t.Fatalf("content origin was lost during normalization: %+v", field) } @@ -57,12 +57,12 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) { t.Fatalf("content origin = %q, want catalog/base.yaml", originPath) } files["catalog/body.md"].Data = []byte("Changed") - again, againInspection, err := state.prompt("child", "v1") + again, againInspection, err := state.prompt("child") if err != nil || again.Templates[0].Content != "Captured {{.name}}" || againInspection.PromptHash != inspection.PromptHash { t.Fatalf("captured definition changed after source mutation: %+v, %+v, %v", again, againInspection, err) } definition.Templates[0].Content = "caller mutation" - third, _, err := state.prompt("child", "v1") + third, _, err := state.prompt("child") if err != nil || third.Templates[0].Content != "Captured {{.name}}" { t.Fatalf("caller mutation escaped into index: %+v, %v", third, err) } @@ -81,12 +81,12 @@ func TestLoadUsesExplicitMappingContextAndClosesContent(t *testing.T) { if err != nil { t.Fatal(err) } - definition, _, err := state.prompt("example", "v1") + definition, _, err := state.prompt("example") if err != nil || definition.Templates[0].Content != "From mapping" || filesystem.contentOpened != 1 || filesystem.contentClosed != 1 { t.Fatalf("mapping context or handle ownership failed: %+v, opens=%d closes=%d, %v", definition, filesystem.contentOpened, filesystem.contentClosed, err) } _, err = load(mapping("contextless", promptWithFile("body.md")), nil) - if err == nil || !strings.Contains(err.Error(), "/prompts/example/v1/messages/0/content_file") { + if err == nil || !strings.Contains(err.Error(), "/prompts/example/messages/0/content_file") { t.Fatalf("expected semantic path for contextless resource, got %v", err) } } @@ -95,7 +95,7 @@ func TestLoadCapturesNativeContentFile(t *testing.T) { root := t.TempDir() definitionPath := filepath.Join(root, "definition.yaml") contentPath := filepath.Join(root, "body.md") - if err := os.WriteFile(definitionPath, []byte("prompts:\n example:\n v1:\n messages:\n - role: user\n content_file: body.md\n output:\n format: text\n validation_mode: basic\n"), 0o600); err != nil { + if err := os.WriteFile(definitionPath, []byte("prompts:\n example:\n version: \"v1\"\n messages:\n - role: user\n content_file: body.md\n output:\n format: text\n validation_mode: basic\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(contentPath, []byte("Native body"), 0o600); err != nil { @@ -108,31 +108,31 @@ func TestLoadCapturesNativeContentFile(t *testing.T) { if err := os.WriteFile(contentPath, []byte("Modified"), 0o600); err != nil { t.Fatal(err) } - definition, _, err := state.prompt("example", "v1") + definition, _, err := state.prompt("example") if err != nil || definition.Templates[0].Content != "Native body" { t.Fatalf("native content was not captured: %+v, %v", definition, err) } } -func TestLoadRejectsMalformedStaticSyntaxAndAmbiguousSelection(t *testing.T) { +func TestLoadRejectsMalformedStaticSyntaxAndCapturesOptionalVersion(t *testing.T) { _, err := load(mapping("bad-template", map[string]any{"prompts": map[string]any{"example": map[string]any{ - "v1": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "{{if}}"}}, - "output": map[string]any{"format": "text", "validation_mode": "basic"}}, + "messages": []any{map[string]any{"role": "user", "content": "{{if}}"}}, + "output": map[string]any{"format": "text", "validation_mode": "basic"}, }}}), nil) if err == nil || !strings.Contains(err.Error(), "invalid template syntax") { t.Fatalf("expected construction-time syntax error, got %v", err) } - state, err := load(mapping("versions", map[string]any{"prompts": map[string]any{"example": map[string]any{ - "v1": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "one"}}, - "output": map[string]any{"format": "text", "validation_mode": "basic"}}, - "v2": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "two"}}, - "output": map[string]any{"format": "text", "validation_mode": "basic"}}, + state, err := load(mapping("version metadata", map[string]any{"prompts": map[string]any{"example": map[string]any{ + "version": "release label", + "messages": []any{map[string]any{"role": "user", "content": "one"}}, + "output": map[string]any{"format": "text", "validation_mode": "basic"}, }}}), nil) if err != nil { t.Fatal(err) } - if _, _, err := state.prompt("example", ""); !errors.Is(err, errPromptAmbiguous) { - t.Fatalf("expected ambiguous version selection, got %v", err) + definition, inspection, err := state.prompt("example") + if err != nil || definition.Version != "release label" || inspection.PromptVersion != "release label" { + t.Fatalf("optional version metadata was not retained: definition=%+v inspection=%+v err=%v", definition, inspection, err) } } @@ -148,30 +148,26 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing. "output": map[string]any{"format": "text", "validation_mode": "none"}, } } + selectedPrompt := makePrompt("selected") + selectedPrompt["version"] = "v2" state, err := load(mapping("prompt inspection lookup", map[string]any{ "profiles": map[string]any{"partial": map[string]any{}}, "prompts": map[string]any{ - "multi": map[string]any{"v1": makePrompt("first"), "v2": makePrompt("second")}, - "single": map[string]any{"v1": makePrompt("only")}, + "selected": selectedPrompt, }, }), nil) if err != nil { t.Fatal(err) } - for _, selection := range []struct { - id, version string - }{ - {id: "multi", version: "v2"}, - {id: "single"}, - } { - inspection, err := state.GetPromptInspection(context.Background(), selection.id, selection.version) + { + inspection, err := state.GetPromptInspection(context.Background(), "selected", "") if err != nil { - t.Fatalf("inspect %q/%q: %v", selection.id, selection.version, err) + t.Fatalf("inspect prompt ID: %v", err) } - definition, hash, err := state.GetPromptSnapshot(context.Background(), selection.id, selection.version) + definition, hash, err := state.GetPromptSnapshot(context.Background(), "selected", "") if err != nil { - t.Fatalf("snapshot %q/%q: %v", selection.id, selection.version, err) + t.Fatalf("snapshot prompt ID: %v", err) } if inspection.PromptID != definition.ID || inspection.PromptVersion != definition.Version || inspection.PromptHash != hash || inspection.DefaultProfileID != "partial" || @@ -184,53 +180,42 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing. } } - owned, err := state.GetPromptInspection(context.Background(), "single", "") + owned, err := state.GetPromptInspection(context.Background(), "selected", "") if err != nil { t.Fatal(err) } owned.Inputs[0].Name = "caller mutation" owned.Inputs[0].Description = "caller mutation" - again, err := state.GetPromptInspection(context.Background(), "single", "v1") + again, err := state.GetPromptInspection(context.Background(), "selected", "") if err != nil || again.Inputs[0].Name != "document" || again.Inputs[0].Description != "source document" { t.Fatalf("inspection mutation escaped into captured state: inspection=%+v err=%v", again, err) } - for _, selection := range []struct { - id, version string - }{ - {id: "missing", version: "v1"}, - {id: "multi", version: "V2"}, - {id: " ", version: "v1"}, - } { - inspection, err := state.GetPromptInspection(context.Background(), selection.id, selection.version) + for _, id := range []string{"missing", " "} { + inspection, err := state.GetPromptInspection(context.Background(), id, "") if inspection != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) { - t.Errorf("inspection selection %q/%q=(%+v, %v), want not found", selection.id, selection.version, inspection, err) + t.Errorf("inspection lookup %q=(%+v, %v), want not found", id, inspection, err) } - definition, _, err := state.GetPromptSnapshot(context.Background(), selection.id, selection.version) + definition, _, err := state.GetPromptSnapshot(context.Background(), id, "") if definition != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) { - t.Errorf("snapshot selection %q/%q=(%+v, %v), want not found", selection.id, selection.version, definition, err) + t.Errorf("snapshot lookup %q=(%+v, %v), want not found", id, definition, err) } } - if _, err := state.GetPromptInspection(context.Background(), "multi", ""); !errors.Is(err, errPromptAmbiguous) { - t.Errorf("ambiguous inspection selection error=%v", err) - } - if _, _, err := state.GetPromptSnapshot(context.Background(), "multi", ""); !errors.Is(err, errPromptAmbiguous) { - t.Errorf("ambiguous snapshot selection error=%v", err) - } ctx, cancel := context.WithCancel(context.Background()) cancel() - if _, err := state.GetPromptInspection(ctx, "single", "v1"); !errors.Is(err, context.Canceled) { + if _, err := state.GetPromptInspection(ctx, "selected", ""); !errors.Is(err, context.Canceled) { t.Errorf("canceled inspection selection error=%v", err) } } func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T) { - state, err := load(mapping("empty-content", map[string]any{"prompts": map[string]any{" exact.id ": map[string]any{"v1": map[string]any{ - "inputs": []any{map[string]any{"name": " note ", "required": true}}, + state, err := load(mapping("empty-content", map[string]any{"prompts": map[string]any{" exact.id ": map[string]any{ + "version": "v1", + "inputs": []any{map[string]any{"name": " note ", "required": true}}, "messages": []any{map[string]any{"role": " USER ", "content": "", "content_file": nil, "cache_control": map[string]any{"type": " ephemeral ", "ttl": "1h "}}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, - }}}}), nil) + }}}), nil) if err != nil { var loadError *configkit.LoadError if errors.As(err, &loadError) { @@ -238,18 +223,19 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T } t.Fatal(err) } - definition, _, err := state.prompt(" exact.id ", "v1") + definition, _, err := state.prompt(" exact.id ") if err != nil || definition.ID != " exact.id " || definition.Templates[0].Content != "" || definition.Templates[0].Role != "user" || definition.Inputs[0].Name != "note" || definition.Templates[0].CacheControl.TTL != "1h" { t.Fatalf("explicit empty content or literal ID was changed: %+v, %v", definition, err) } - if _, _, err := state.prompt("exact.id", "v1"); !errors.Is(err, errPromptNotFound) { + if _, _, err := state.prompt("exact.id"); !errors.Is(err, errPromptNotFound) { t.Fatalf("trimmed prompt identity unexpectedly matched: %v", err) } - _, err = load(mapping("obsolete-field", map[string]any{"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{ - "id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}}, + _, err = load(mapping("obsolete-field", map[string]any{"prompts": map[string]any{"example": map[string]any{ + "version": "v1", + "id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, - }}}}), nil) + }}}), nil) var loadError *configkit.LoadError if !errors.As(err, &loadError) { t.Fatalf("obsolete identity field should fail strict decoding: %v", err) @@ -262,7 +248,8 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testing.T) { state, err := load(mapping("requirement-metadata", map[string]any{ - "prompts": map[string]any{"example": map[string]any{"v1": map[string]any{ + "prompts": map[string]any{"example": map[string]any{ + "version": "v1", "session_id": "{{.session}}", "inputs": []any{ map[string]any{"name": "absent"}, @@ -272,7 +259,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi }, "messages": []any{map[string]any{"role": "user", "content": "{{input \"absent\"}}"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, - }}}, + }}, }), nil) if err != nil { var loadError *configkit.LoadError @@ -282,7 +269,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi t.Fatal(err) } - snapshot, ok := state.GetRequirementSnapshot("example", "v1") + snapshot, ok := state.GetRequirementSnapshot("example", "") if !ok || len(snapshot.Inputs) != 4 || len(snapshot.Templates.Messages) != 1 || snapshot.Templates.Session == nil { t.Fatalf("requirement snapshot was not captured: %+v, %t", snapshot, ok) } @@ -300,7 +287,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi t.Errorf("snapshot did not use normalized final metadata: %+v", snapshot.Inputs[3]) } - definition, inspection, err := state.prompt("example", "v1") + definition, inspection, err := state.prompt("example") if err != nil { t.Fatal(err) } @@ -312,13 +299,10 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi *snapshot.Inputs[2].ContentType = "caller mutation" snapshot.Inputs[3].Name = "caller mutation" snapshot.Templates.Messages[0] = nil - again, ok := state.GetRequirementSnapshot("example", "v1") + again, ok := state.GetRequirementSnapshot("example", "") if !ok || *again.Inputs[2].ContentType != "" || again.Inputs[3].Name != "normalized" || again.Templates.Messages[0] == nil { t.Fatalf("snapshot mutation escaped into captured state: %+v, %t", again, ok) } - if _, ok := state.GetRequirementSnapshot("example", "V1"); ok { - t.Fatal("snapshot lookup unexpectedly changed exact version matching") - } } func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) { @@ -338,11 +322,12 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) input[name] = value } state, err := load(mapping("hash-"+variant.name, map[string]any{ - "prompts": map[string]any{"same": map[string]any{"v1": map[string]any{ + "prompts": map[string]any{"same": map[string]any{ + "version": "v1", "inputs": []any{input}, "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, - }}}, + }}, }), nil) if err != nil { var loadError *configkit.LoadError @@ -351,7 +336,7 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) } t.Fatal(err) } - _, inspection, err := state.prompt("same", "v1") + _, inspection, err := state.prompt("same") if err != nil { t.Fatal(err) } @@ -368,10 +353,10 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) } func promptWithFile(name string) map[string]any { - return map[string]any{"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{ + return map[string]any{"prompts": map[string]any{"example": map[string]any{ "messages": []any{map[string]any{"role": "user", "content_file": name}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, - }}}} + }}} } type countingFS struct { diff --git a/internal/usecase/smoke_selection_test.go b/internal/usecase/smoke_selection_test.go index 84afb33..6a1b39c 100644 --- a/internal/usecase/smoke_selection_test.go +++ b/internal/usecase/smoke_selection_test.go @@ -356,22 +356,20 @@ func smokeSelectionConfig() map[string]any { }, "prompts": map[string]any{ "selection.prompt": map[string]any{ - "v1": map[string]any{ - "default_profile": "prompt-profile", - "inputs": []any{map[string]any{"name": "required_input", "required": true}}, - "messages": []any{map[string]any{"role": "user", "content": "Hello {{.required_input}}"}}, - "session_id": "{{.vars.session}}", - "output": map[string]any{ - "format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json", - }, + "version": "v1", + "default_profile": "prompt-profile", + "inputs": []any{map[string]any{"name": "required_input", "required": true}}, + "messages": []any{map[string]any{"role": "user", "content": "Hello {{.required_input}}"}}, + "session_id": "{{.vars.session}}", + "output": map[string]any{ + "format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json", }, }, "backend.prompt": map[string]any{ - "v1": map[string]any{ - "messages": []any{map[string]any{"role": "user", "content": "Hello"}}, - "output": map[string]any{ - "format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json", - }, + "version": "v1", + "messages": []any{map[string]any{"role": "user", "content": "Hello"}}, + "output": map[string]any{ + "format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json", }, }, }, diff --git a/listing_contract_test.go b/listing_contract_test.go index 2c52db1..588d74b 100644 --- a/listing_contract_test.go +++ b/listing_contract_test.go @@ -18,10 +18,7 @@ func TestListIdentitiesBasicContract(t *testing.T) { engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{ Source: configkit.Mapping("listing", map[string]any{ "profiles": map[string]any{"configured-profile": map[string]any{"model": "example-model"}}, - "prompts": map[string]any{"example-prompt": map[string]any{ - "2": listingContractPrompt(), - "10": listingContractPrompt(), - }}, + "prompts": map[string]any{"example-prompt": promptDefinitionWithVersion("10", listingContractPrompt())}, }), }}}) if err != nil { @@ -42,7 +39,6 @@ func TestListIdentitiesBasicContract(t *testing.T) { } want := []promptkit.PromptIdentity{ {ID: "example-prompt", Version: "10"}, - {ID: "example-prompt", Version: "2"}, } if len(prompts) != len(want) { t.Fatalf("prompt listing = %#v, want %#v", prompts, want) @@ -80,7 +76,7 @@ func TestListIdentitiesPreserveContextErrorsWithoutPartialResults(t *testing.T) engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{ Source: configkit.Mapping("listing", map[string]any{ "profiles": map[string]any{"profile": map[string]any{}}, - "prompts": map[string]any{"example": map[string]any{"v1": listingContractPrompt()}}, + "prompts": map[string]any{"example": promptDefinitionWithVersion("v1", listingContractPrompt())}, }), }}}) if err != nil { @@ -120,11 +116,8 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) { "overlay/profile": map[string]any{"endpoint": "https://example.test/v1", "model": "old-model"}, }, "prompts": map[string]any{ - "shared/prompt": map[string]any{ - "v1": listingContractPrompt(), - "v2": listingContractPrompt(), - }, - "underlay-only": map[string]any{"v1": listingContractPrompt()}, + "shared/prompt": promptDefinitionWithVersion("v1", listingContractPrompt()), + "underlay-only": promptDefinitionWithVersion("v1", listingContractPrompt()), }, })} main := configkit.Input{Source: configkit.Mapping("application overlay", map[string]any{ @@ -132,8 +125,8 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) { "overlay/profile": map[string]any{"model": "effective-model"}, }, "prompts": map[string]any{ - "shared/prompt": map[string]any{"v1": listingContractPrompt()}, - "main-only": map[string]any{"v3": listingContractPrompt()}, + "shared/prompt": promptDefinitionWithVersion("v2", listingContractPrompt()), + "main-only": promptDefinitionWithVersion("v3", listingContractPrompt()), }, })} engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ @@ -177,14 +170,13 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) { } wantPrompts := []promptkit.PromptIdentity{ {ID: "main-only", Version: "v3"}, - {ID: "shared/prompt", Version: "v1"}, {ID: "shared/prompt", Version: "v2"}, {ID: "underlay-only", Version: "v1"}, } if !reflect.DeepEqual(prompts, wantPrompts) { t.Fatalf("effective prompt identities = %#v, want %#v", prompts, wantPrompts) } - if got := countListingPrompt(prompts, "shared/prompt", "v1"); got != 1 { + if got := countListingPrompt(prompts, "shared/prompt", "v2"); got != 1 { t.Fatalf("overlaid prompt identity appears %d times, want 1", got) } } @@ -211,7 +203,7 @@ func TestListIdentitiesEmptyEffectiveConfiguration(t *testing.T) { func TestListIdentitiesAreOwnedAndSafeForConcurrentUse(t *testing.T) { engine, err := newListingContractEngine(t, map[string]any{ "profiles": map[string]any{"profile": map[string]any{"model": "partial-model"}}, - "prompts": map[string]any{"prompt": map[string]any{"v1": listingContractPrompt()}}, + "prompts": map[string]any{"prompt": promptDefinitionWithVersion("v1", listingContractPrompt())}, }) if err != nil { t.Fatal(err) @@ -300,7 +292,7 @@ func TestListIdentitiesAreOwnedAndSafeForConcurrentUse(t *testing.T) { func TestListIdentitiesUseCapturedSourcesWithoutExecutionWork(t *testing.T) { files := fstest.MapFS{ - "config/listing.yaml": &fstest.MapFile{Data: []byte("profiles:\n captured-partial:\n model: partial-model\nprompts:\n captured-prompt:\n v1:\n messages:\n - role: user\n content: Hello.\n output:\n format: text\n validation_mode: basic\n")}, + "config/listing.yaml": &fstest.MapFile{Data: []byte("profiles:\n captured-partial:\n model: partial-model\nprompts:\n captured-prompt:\n version: v1\n messages:\n - role: user\n content: Hello.\n output:\n format: text\n validation_mode: basic\n")}, } inputs, err := promptkit.FilesystemInputs("application listing", files, "config") if err != nil { @@ -312,7 +304,7 @@ func TestListIdentitiesUseCapturedSourcesWithoutExecutionWork(t *testing.T) { if err != nil { t.Fatal(err) } - files["config/listing.yaml"].Data = []byte("profiles:\n replacement-profile: {}\nprompts:\n replacement-prompt:\n v2:\n messages:\n - role: user\n content: Changed.\n output:\n format: text\n validation_mode: basic\n") + files["config/listing.yaml"].Data = []byte("profiles:\n replacement-profile: {}\nprompts:\n replacement-prompt:\n version: v2\n messages:\n - role: user\n content: Changed.\n output:\n format: text\n validation_mode: basic\n") profiles, err := engine.ListProfiles(context.Background()) if err != nil || !containsListingProfile(profiles, "captured-partial") || containsListingProfile(profiles, "replacement-profile") { diff --git a/observation_boundary_contract_test.go b/observation_boundary_contract_test.go index 2a09984..9807b6f 100644 --- a/observation_boundary_contract_test.go +++ b/observation_boundary_contract_test.go @@ -27,11 +27,12 @@ func TestDelegatedOperationsIsolateObservations(t *testing.T) { child, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{ Source: configkit.Mapping("child execution", map[string]any{ "profiles": map[string]any{"profile": map[string]any{"endpoint": "https://example.invalid/v1", "model": "child-model"}}, - "prompts": map[string]any{"child": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"child": map[string]any{ + "version": "1", "default_profile": "profile", "messages": []any{map[string]any{"role": "user", "content": "child message"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}, + }}, }), }}}, promptkit.WithLLMClient(observationClientFunc(func(callCtx context.Context, _ promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) { gotDeadline, ok := callCtx.Deadline() diff --git a/prepared_execution_contract_test.go b/prepared_execution_contract_test.go index cc2e89d..52d0104 100644 --- a/prepared_execution_contract_test.go +++ b/prepared_execution_contract_test.go @@ -767,11 +767,12 @@ func preparedContractConfig(message string, keyRequired bool) promptkit.Config { "profiles": map[string]any{"profile": map[string]any{ "endpoint": "http://example.test/v1", "model": "model", "api_key_required": keyRequired, }}, - "prompts": map[string]any{"prepared": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"prepared": map[string]any{ + "version": "1", "default_profile": "profile", "messages": []any{map[string]any{"role": "user", "content": message}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}, + }}, }), }}} } @@ -780,19 +781,19 @@ func preparedPromptSource(label string) fstest.MapFS { return fstest.MapFS{ "prompt.yaml": &fstest.MapFile{Data: []byte(`prompts: prepared: - "1": - default_profile: profile - inputs: - - name: input - required: true - messages: - - role: user - content: 'Input={{input "input"}} Label={{.label}}' - description: ` + label + ` - output: - format: json - validation_mode: json_schema - schema_path: schema.json + version: "1" + default_profile: profile + inputs: + - name: input + required: true + messages: + - role: user + content: 'Input={{input "input"}} Label={{.label}}' + description: ` + label + ` + output: + format: json + validation_mode: json_schema + schema_path: schema.json `)}, } } diff --git a/prepared_observation_contract_test.go b/prepared_observation_contract_test.go index 5ef59db..cbeb1ae 100644 --- a/prepared_observation_contract_test.go +++ b/prepared_observation_contract_test.go @@ -137,11 +137,12 @@ func TestPreparedObservationFinishesAfterCredentialAndCapacityFailures(t *testin "profiles": map[string]any{"profile": map[string]any{ "endpoint": "https://example.invalid/v1", "model": "model", "api_key_required": true, }}, - "prompts": map[string]any{"prepared": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"prepared": map[string]any{ + "version": "1", "default_profile": "profile", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}, + }}, }), }}}, promptkit.WithLLMClient(&fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}})) if err != nil { diff --git a/profile_inheritance_contract_test.go b/profile_inheritance_contract_test.go index a2cdafb..085d118 100644 --- a/profile_inheritance_contract_test.go +++ b/profile_inheritance_contract_test.go @@ -13,11 +13,11 @@ import ( ) func aliasPrompt() map[string]any { - return map[string]any{"prompts": map[string]any{"prepared": map[string]any{"1": map[string]any{ + return map[string]any{"prompts": map[string]any{"prepared": promptDefinitionWithVersion("1", map[string]any{ "default_profile": "child", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}} + })}} } func TestProfileInheritanceBuiltInAliasWorkflow(t *testing.T) { diff --git a/profile_inspection_contract_test.go b/profile_inspection_contract_test.go index 6f57103..86d58c1 100644 --- a/profile_inspection_contract_test.go +++ b/profile_inspection_contract_test.go @@ -20,11 +20,12 @@ func TestInspectProfileUsesLiteralFileKeyForPreparation(t *testing.T) { engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ Underlays: []configkit.Underlay{{Inputs: profiles}}, Main: &configkit.Input{Source: configkit.Mapping("prompt", map[string]any{ - "prompts": map[string]any{"prompt": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"prompt": map[string]any{ + "version": "1", "default_profile": " literal ", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}, + }}, })}, }}) if err != nil { diff --git a/reliable_execution_contract_test.go b/reliable_execution_contract_test.go index f468667..965f493 100644 --- a/reliable_execution_contract_test.go +++ b/reliable_execution_contract_test.go @@ -86,11 +86,12 @@ func reliabilityConfig(backends, profiles map[string]any) promptkit.Config { return promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("reliable execution", map[string]any{ "backends": backends, "profiles": profiles, - "prompts": map[string]any{"prompt": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"prompt": map[string]any{ + "version": "1", "default_profile": "profile-a", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, - }}}, + }}, })}}} } @@ -252,14 +253,14 @@ func TestPreparedRetriesFreezeSourceAndCredentialAtExecution(t *testing.T) { t.Setenv(envName, "first-key") source := fstest.MapFS{"prompt.yaml": &fstest.MapFile{Data: []byte(`prompts: prompt: - "1": - default_profile: profile - messages: - - role: user - content: original message - output: - format: text - validation_mode: none + version: "1" + default_profile: profile + messages: + - role: user + content: original message + output: + format: text + validation_mode: none `)}} var calls int var auth, content []string @@ -306,14 +307,14 @@ func TestPreparedRetriesFreezeSourceAndCredentialAtExecution(t *testing.T) { } source["prompt.yaml"] = &fstest.MapFile{Data: []byte(`prompts: prompt: - "1": - default_profile: profile - messages: - - role: user - content: changed message - output: - format: text - validation_mode: none + version: "1" + default_profile: profile + messages: + - role: user + content: changed message + output: + format: text + validation_mode: none `)} result, err := engine.RunPrepared(context.Background(), prepared) if err != nil || result == nil || calls != 2 { diff --git a/requirements_inspection_contract_test.go b/requirements_inspection_contract_test.go index bae54eb..97a5f88 100644 --- a/requirements_inspection_contract_test.go +++ b/requirements_inspection_contract_test.go @@ -52,7 +52,16 @@ func contractPrompt(defaultProfile string, content string, inputs []any, session } func oneVersionPrompt(id, version string, definition map[string]any) map[string]any { - return map[string]any{id: map[string]any{version: definition}} + return map[string]any{id: promptDefinitionWithVersion(version, definition)} +} + +func promptDefinitionWithVersion(version string, definition map[string]any) map[string]any { + result := make(map[string]any, len(definition)+1) + result["version"] = version + for key, value := range definition { + result[key] = value + } + return result } var noValidationOutput = map[string]any{"format": "text", "validation_mode": "none"} @@ -123,16 +132,12 @@ func TestInspectRequirementsEnumeratesCapturedDependenciesAndInactiveNames(t *te } } -func TestInspectRequirementsSelectionUsesExactVersionsAndProfileIdentity(t *testing.T) { +func TestInspectRequirementsUsesPromptIDsAndProfileIdentity(t *testing.T) { prompts := map[string]any{ - "multi": map[string]any{ - "v1": contractPrompt("first", `{{input "one"}}`, nil, "", noValidationOutput), - "v2": contractPrompt("second", `{{input "two"}}`, nil, "", noValidationOutput), - }, - "without-default": map[string]any{ - "v1": contractPrompt("", "plain", nil, "", noValidationOutput), - }, - "direct-required": map[string]any{"v1": contractPrompt("direct", "plain", nil, "", noValidationOutput)}, + "first-prompt": promptDefinitionWithVersion("v1", contractPrompt("first", `{{input "one"}}`, nil, "", noValidationOutput)), + "second-prompt": promptDefinitionWithVersion("v2", contractPrompt("second", `{{input "two"}}`, nil, "", noValidationOutput)), + "without-default": contractPrompt("", "plain", nil, "", noValidationOutput), + "direct-required": promptDefinitionWithVersion("v1", contractPrompt("direct", "plain", nil, "", noValidationOutput)), } profiles := map[string]any{ "first": map[string]any{"model": "first-model"}, @@ -140,26 +145,20 @@ func TestInspectRequirementsSelectionUsesExactVersionsAndProfileIdentity(t *test "direct": map[string]any{"endpoint": "https://example.test/v1", "model": "direct-model", "api_key_required": true}, } engine := requirementsEngine(t, prompts, profiles) - if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi"}); !errors.Is(err, promptkit.ErrPromptLoad) { - t.Fatalf("ambiguous omitted version error=%v", err) - } - first, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "v1"}) + first, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "first-prompt"}) if err != nil || first.PromptVersion != "v1" || first.ProfileID != "first" || len(first.Inputs) != 1 || first.Inputs[0].Name != "one" { - t.Fatalf("exact first version/default profile=(%+v, %v)", first, err) + t.Fatalf("first prompt ID/default profile=(%+v, %v)", first, err) } - explicit, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{ - PromptID: "multi", PromptVersion: "v2", ProfileID: "first", + second, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{ + PromptID: "second-prompt", ProfileID: "first", }) - if err != nil || explicit.PromptVersion != "v2" || explicit.ProfileID != "first" || explicit.Inputs[0].Name != "two" { - t.Fatalf("exact version/request profile=(%+v, %v)", explicit, err) + if err != nil || second.PromptVersion != "v2" || second.ProfileID != "first" || second.Inputs[0].Name != "two" { + t.Fatalf("second prompt ID/request profile=(%+v, %v)", second, err) } - if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "Multi", PromptVersion: "v1"}); !errors.Is(err, promptkit.ErrPromptNotFound) { + if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "First-Prompt"}); !errors.Is(err, promptkit.ErrPromptNotFound) { t.Fatalf("case-folded prompt ID unexpectedly selected: %v", err) } - if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "V1"}); !errors.Is(err, promptkit.ErrPromptNotFound) { - t.Fatalf("case-folded version unexpectedly selected: %v", err) - } - if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "v1", ProfileID: "missing"}); !errors.Is(err, promptkit.ErrProfileNotFound) { + if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "first-prompt", ProfileID: "missing"}); !errors.Is(err, promptkit.ErrProfileNotFound) { t.Fatalf("missing profile error=%v", err) } if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "without-default"}); !errors.Is(err, promptkit.ErrProfileRequired) || !errors.Is(err, promptkit.ErrInvalidRequest) { @@ -173,7 +172,7 @@ func TestInspectRequirementsSelectionUsesExactVersionsAndProfileIdentity(t *test } ctx, cancel := context.WithCancel(context.Background()) cancel() - if _, err := engine.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "v1"}); !errors.Is(err, context.Canceled) { + if _, err := engine.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{PromptID: "first-prompt"}); !errors.Is(err, context.Canceled) { t.Fatalf("canceled inspection error=%v", err) } } @@ -202,11 +201,11 @@ func TestInspectPromptHashMatchesPreparationForCapturedConfiguration(t *testing. func TestInspectRequirementsReportsConditionalAndCompleteOptionalInventories(t *testing.T) { prompts := map[string]any{ - "conditional": map[string]any{"v1": contractPrompt("profile", - `{{if .mode}}{{input "left"}}{{else}}{{input "right"}}{{end}}`, nil, "", noValidationOutput)}, - "optional": map[string]any{"v1": contractPrompt("profile", `{{index . "lookup"}}`, - []any{map[string]any{"name": "declared-optional"}}, "", noValidationOutput)}, - "empty": map[string]any{"v1": contractPrompt("profile", "plain", nil, "", noValidationOutput)}, + "conditional": promptDefinitionWithVersion("v1", contractPrompt("profile", + `{{if .mode}}{{input "left"}}{{else}}{{input "right"}}{{end}}`, nil, "", noValidationOutput)), + "optional": promptDefinitionWithVersion("v1", contractPrompt("profile", `{{index . "lookup"}}`, + []any{map[string]any{"name": "declared-optional"}}, "", noValidationOutput)), + "empty": promptDefinitionWithVersion("v1", contractPrompt("profile", "plain", nil, "", noValidationOutput)), } engine := requirementsEngine(t, prompts, map[string]any{"profile": map[string]any{"model": "partial"}}) conditional, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "conditional"}) diff --git a/smoke_integration_contract_test.go b/smoke_integration_contract_test.go index 8b1a6e8..f1350d4 100644 --- a/smoke_integration_contract_test.go +++ b/smoke_integration_contract_test.go @@ -252,7 +252,8 @@ func TestSmokeTestUsesPrivateInferenceWithoutApplicationResourcesOrListingChange client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: outputMarker}} input := configkit.Input{Source: configkit.Mapping("private diagnostic test", map[string]any{ "profiles": map[string]any{"profile": map[string]any{"endpoint": "http://example.test/v1", "model": "model"}}, - "prompts": map[string]any{"application-prompt": map[string]any{"1": map[string]any{ + "prompts": map[string]any{"application-prompt": map[string]any{ + "version": "1", "default_profile": "profile", "session_id": `application-session {{input "document"}}`, "inputs": []any{map[string]any{"name": "document", "required": true}}, @@ -263,7 +264,7 @@ func TestSmokeTestUsesPrivateInferenceWithoutApplicationResourcesOrListingChange "output": map[string]any{ "format": "json", "validation_mode": "json_schema", "schema_path": schemaMarker, }, - }}}, + }}, })} engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, promptkit.WithLLMClient(client), promptkit.WithArtifactReader(reader), promptkit.WithSchemaFS(schemaSource, "schemas"))