diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a34eec5..36036a4 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implement PromptKit requirements inspection -Status: in progress; Stages 1 through 4 are complete. +Status: in progress; Stages 1 through 5 are complete. ## Authority and working rules @@ -365,6 +365,20 @@ Run the composed example's tests and both commands from its directory. Record actual report semantics, not explanatory wording snapshots. Documentation of these copyable commands belongs in Stage 6. +**Completed:** Extended the synthetic prompt in +`integrations/ckprompt/examples/composed/main.go` with the conditional +`notes` dependency while retaining the declared `topic` input. Added assembled +App coverage in `main_test.go` for selected spring identity, missing and +supplied `mode`, conditional versus required classification of undeclared +`notes`, incomplete versus complete discovery, and success for both reports. +`GOTOOLCHAIN=local go test ./examples/composed -run +'TestComposedRequirementsDiscoveryUsesSelectedCampaign$' -count=1` and +`GOTOOLCHAIN=local go test ./examples/composed` passed from +`integrations/ckprompt`. Both roadmap commands passed from +`integrations/ckprompt/examples/composed`: the first report was incomplete with +a missing `mode` issue and conditional `notes`; the second was complete with +`mode` supplied and required `notes`. No material limitations were encountered. + ## Stage 6: Align canonical documentation with the delivered feature **Outcome:** API, operator, and consumer documentation describes the implemented diff --git a/integrations/ckprompt/examples/composed/main.go b/integrations/ckprompt/examples/composed/main.go index 71a2e01..95e8dd7 100644 --- a/integrations/ckprompt/examples/composed/main.go +++ b/integrations/ckprompt/examples/composed/main.go @@ -121,7 +121,7 @@ func loadPromptEngine(ctx context.Context, selection ckconfig.Selection, input * chosen.PromptVersion: map[string]any{ "default_profile": chosen.ProfileID, "inputs": []any{map[string]any{"name": "topic", "required": true, "content_type": "text/plain", "description": "Topic to brief"}}, - "messages": []any{map[string]any{"role": "user", "content": "Write a concise briefing about {{.topic}}."}}, + "messages": []any{map[string]any{"role": "user", "content": "Write a concise briefing about {{.topic}}. {{if .mode}}{{input \"notes\"}}{{end}}"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}, }, }}, diff --git a/integrations/ckprompt/examples/composed/main_test.go b/integrations/ckprompt/examples/composed/main_test.go index 85e316e..db89761 100644 --- a/integrations/ckprompt/examples/composed/main_test.go +++ b/integrations/ckprompt/examples/composed/main_test.go @@ -117,6 +117,100 @@ func TestComposedPromptKitAndDomainCommandsRemainAvailable(t *testing.T) { } } +func TestComposedRequirementsDiscoveryUsesSelectedCampaign(t *testing.T) { + app, err := newApp() + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + name string + args []string + wantMode bool + wantModeIssue bool + }{ + { + name: "conditional dependency is unresolved without mode", + args: []string{"promptkit", "requirements", "show", "briefing", "--campaign", "spring"}, + wantModeIssue: true, + }, + { + name: "supplied mode discovers conditional notes", + args: []string{"promptkit", "requirements", "show", "briefing", "--campaign", "spring", "--var", "mode=full", "--format", "yaml"}, + wantMode: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + var output bytes.Buffer + outcome := app.Execute(context.Background(), test.args, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 { + t.Fatalf("requirements outcome = %#v, report %q", outcome, output.String()) + } + + var report struct { + PromptID string `yaml:"prompt_id"` + PromptVersion string `yaml:"prompt_version"` + ProfileID string `yaml:"profile_id"` + Complete bool `yaml:"complete"` + Inputs []struct { + Name string `yaml:"name"` + Kind string `yaml:"kind"` + Declared bool `yaml:"declared"` + } `yaml:"inputs"` + Variables []struct { + Name string `yaml:"name"` + Supplied bool `yaml:"supplied"` + } `yaml:"variables"` + Issues []struct { + Code string `yaml:"code"` + InputName string `yaml:"input_name"` + VariableName string `yaml:"variable_name"` + } `yaml:"issues"` + } + if err := yaml.Unmarshal(output.Bytes(), &report); err != nil { + t.Fatalf("decode requirements report %q: %v", output.String(), err) + } + if report.PromptID != "briefing" || report.PromptVersion != "1" || report.ProfileID != "spring" { + t.Fatalf("requirements identity = %#v", report) + } + + foundTopic, foundNotes, foundMode := false, false, false + for _, input := range report.Inputs { + switch input.Name { + case "topic": + foundTopic = input.Declared && input.Kind == "required" + case "notes": + foundNotes = true + wantKind := "conditional" + if test.wantMode { + wantKind = "required" + } + if input.Declared || input.Kind != wantKind { + t.Errorf("notes input classification = %#v; want undeclared %s", input, wantKind) + } + } + } + for _, variable := range report.Variables { + if variable.Name == "mode" { + foundMode = variable.Supplied == test.wantMode + } + } + if !foundTopic || !foundNotes || !foundMode { + t.Fatalf("requirements inventory topic=%t notes=%t mode=%t; report %#v", foundTopic, foundNotes, foundMode, report) + } + if test.wantModeIssue && (len(report.Issues) != 1 || report.Issues[0].Code != "missing_variable" || report.Issues[0].VariableName != "mode") { + t.Fatalf("missing mode issue = %#v", report.Issues) + } + if !test.wantModeIssue && len(report.Issues) != 0 { + t.Fatalf("requirements issues with mode supplied=%t: %#v", test.wantMode, report.Issues) + } + if report.Complete != test.wantMode { + t.Fatalf("requirements completeness = %t with mode supplied=%t; report %#v", report.Complete, test.wantMode, report) + } + }) + } +} + func TestPromptKitScopeDiffAndCatalogsUseSelectedCampaign(t *testing.T) { app, err := newApp() if err != nil {