Expose ConfigKit source evidence in reports
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Dependency upgrades, discovery, and smoke testing implementation plan
|
# Dependency upgrades, discovery, and smoke testing implementation plan
|
||||||
|
|
||||||
Status: Stages 1–2 complete; Stage 3 is next.
|
Status: Stages 1–3 complete; Stage 4 is next.
|
||||||
|
|
||||||
## Authority and baseline
|
## Authority and baseline
|
||||||
|
|
||||||
@@ -271,6 +271,12 @@ or whole-output snapshots. Run focused source tests and the ckconfig suite.
|
|||||||
**Complete when:** both source formats expose the agreed native evidence with no
|
**Complete when:** both source formats expose the agreed native evidence with no
|
||||||
parallel ConfigKit behavior.
|
parallel ConfigKit behavior.
|
||||||
|
|
||||||
|
**Completed:** 2026-10-17. YAML and text `config sources` now project native
|
||||||
|
destination, placement, unresolved-destination, and source-version evidence.
|
||||||
|
Focused semantic-YAML and text-field assertions cover the requested presence,
|
||||||
|
null, redaction, location, and placement cases. Focused source tests and the
|
||||||
|
complete ckconfig suite passed.
|
||||||
|
|
||||||
## Stage 4: Migrate ckprompt production code to PromptKit v0.16.0
|
## Stage 4: Migrate ckprompt production code to PromptKit v0.16.0
|
||||||
|
|
||||||
**Primary files:** ckprompt manifest/sums, `adapters.go`, `command.go`,
|
**Primary files:** ckprompt manifest/sums, `adapters.go`, `command.go`,
|
||||||
|
|||||||
@@ -17,13 +17,17 @@ type sourceReport struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type sourceDTO struct {
|
type sourceDTO struct {
|
||||||
ID string `yaml:"id"`
|
ID string `yaml:"id"`
|
||||||
Label string `yaml:"label"`
|
Label string `yaml:"label"`
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
Layer string `yaml:"layer"`
|
Destination string `yaml:"destination"`
|
||||||
Kind string `yaml:"kind"`
|
Placement string `yaml:"placement"`
|
||||||
UnderlayIndex int `yaml:"underlay_index"`
|
DestinationUnresolved bool `yaml:"destination_unresolved"`
|
||||||
ImportedFrom *locationDTO `yaml:"imported_from"`
|
Layer string `yaml:"layer"`
|
||||||
|
Kind string `yaml:"kind"`
|
||||||
|
UnderlayIndex int `yaml:"underlay_index"`
|
||||||
|
ImportedFrom *locationDTO `yaml:"imported_from"`
|
||||||
|
VersionCheck *sourceVersionCheckDTO `yaml:"version_check"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type locationDTO struct {
|
type locationDTO struct {
|
||||||
@@ -32,6 +36,19 @@ type locationDTO struct {
|
|||||||
Column int `yaml:"column"`
|
Column int `yaml:"column"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sourceVersionCheckDTO struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Version versionDTO `yaml:"version"`
|
||||||
|
Location *locationDTO `yaml:"location"`
|
||||||
|
Redacted bool `yaml:"redacted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type versionDTO struct {
|
||||||
|
Present bool `yaml:"present"`
|
||||||
|
Null bool `yaml:"null"`
|
||||||
|
Text string `yaml:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
||||||
selected := plan.scopes[0]
|
selected := plan.scopes[0]
|
||||||
name := selected.scope.scope.Name
|
name := selected.scope.scope.Name
|
||||||
@@ -59,17 +76,39 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
|
|||||||
sources := acquired.Value.Sources.Sources()
|
sources := acquired.Value.Sources.Sources()
|
||||||
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
|
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
|
placement := "mapping"
|
||||||
|
if source.PlacementMode == configkit.PlacementModeSequenceItem {
|
||||||
|
placement = "sequence_item"
|
||||||
|
}
|
||||||
item := sourceDTO{
|
item := sourceDTO{
|
||||||
ID: source.ID,
|
ID: source.ID,
|
||||||
Label: source.Label,
|
Label: source.Label,
|
||||||
Path: source.Path,
|
Path: source.Path,
|
||||||
Layer: string(source.Layer),
|
Destination: source.Destination.String(),
|
||||||
Kind: string(source.Kind),
|
Placement: placement,
|
||||||
UnderlayIndex: source.UnderlayIndex,
|
DestinationUnresolved: source.DestinationUnresolved,
|
||||||
|
Layer: string(source.Layer),
|
||||||
|
Kind: string(source.Kind),
|
||||||
|
UnderlayIndex: source.UnderlayIndex,
|
||||||
}
|
}
|
||||||
if source.ImportedFrom != nil {
|
if source.ImportedFrom != nil {
|
||||||
item.ImportedFrom = &locationDTO{SourceID: source.ImportedFrom.SourceID, Line: source.ImportedFrom.Line, Column: source.ImportedFrom.Column}
|
item.ImportedFrom = &locationDTO{SourceID: source.ImportedFrom.SourceID, Line: source.ImportedFrom.Line, Column: source.ImportedFrom.Column}
|
||||||
}
|
}
|
||||||
|
if source.VersionCheck != nil {
|
||||||
|
check := source.VersionCheck
|
||||||
|
item.VersionCheck = &sourceVersionCheckDTO{
|
||||||
|
Path: check.Path.String(),
|
||||||
|
Version: versionDTO{
|
||||||
|
Present: check.Version.Present,
|
||||||
|
Null: check.Version.Null,
|
||||||
|
Text: check.Version.Text,
|
||||||
|
},
|
||||||
|
Redacted: check.Redacted,
|
||||||
|
}
|
||||||
|
if check.Location != nil {
|
||||||
|
item.VersionCheck.Location = &locationDTO{SourceID: check.Location.SourceID, Line: check.Location.Line, Column: check.Location.Column}
|
||||||
|
}
|
||||||
|
}
|
||||||
document.Sources = append(document.Sources, item)
|
document.Sources = append(document.Sources, item)
|
||||||
}
|
}
|
||||||
var report bytes.Buffer
|
var report bytes.Buffer
|
||||||
@@ -102,6 +141,9 @@ func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {
|
|||||||
_, _ = fmt.Fprintf(writer, " - id: %s\n", strconv.Quote(source.ID))
|
_, _ = fmt.Fprintf(writer, " - id: %s\n", strconv.Quote(source.ID))
|
||||||
_, _ = fmt.Fprintf(writer, " label: %s\n", strconv.Quote(source.Label))
|
_, _ = fmt.Fprintf(writer, " label: %s\n", strconv.Quote(source.Label))
|
||||||
_, _ = fmt.Fprintf(writer, " path: %s\n", strconv.Quote(source.Path))
|
_, _ = fmt.Fprintf(writer, " path: %s\n", strconv.Quote(source.Path))
|
||||||
|
_, _ = fmt.Fprintf(writer, " destination: %s\n", strconv.Quote(source.Destination))
|
||||||
|
_, _ = fmt.Fprintf(writer, " placement: %s\n", strconv.Quote(source.Placement))
|
||||||
|
_, _ = fmt.Fprintf(writer, " destination_unresolved: %t\n", source.DestinationUnresolved)
|
||||||
_, _ = fmt.Fprintf(writer, " layer: %s\n", strconv.Quote(source.Layer))
|
_, _ = fmt.Fprintf(writer, " layer: %s\n", strconv.Quote(source.Layer))
|
||||||
_, _ = fmt.Fprintf(writer, " kind: %s\n", strconv.Quote(source.Kind))
|
_, _ = fmt.Fprintf(writer, " kind: %s\n", strconv.Quote(source.Kind))
|
||||||
_, _ = fmt.Fprintf(writer, " underlay_index: %d\n", source.UnderlayIndex)
|
_, _ = fmt.Fprintf(writer, " underlay_index: %d\n", source.UnderlayIndex)
|
||||||
@@ -111,6 +153,21 @@ func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {
|
|||||||
_, _ = fmt.Fprintf(writer, " imported_from: {source_id: %s, line: %d, column: %d}\n",
|
_, _ = fmt.Fprintf(writer, " imported_from: {source_id: %s, line: %d, column: %d}\n",
|
||||||
strconv.Quote(source.ImportedFrom.SourceID), source.ImportedFrom.Line, source.ImportedFrom.Column)
|
strconv.Quote(source.ImportedFrom.SourceID), source.ImportedFrom.Line, source.ImportedFrom.Column)
|
||||||
}
|
}
|
||||||
|
if source.VersionCheck == nil {
|
||||||
|
_, _ = writer.WriteString(" version_check: null\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
check := source.VersionCheck
|
||||||
|
_, _ = fmt.Fprintf(writer, " version_check:\n path: %s\n", strconv.Quote(check.Path))
|
||||||
|
_, _ = fmt.Fprintf(writer, " version:\n present: %t\n null: %t\n text: %s\n",
|
||||||
|
check.Version.Present, check.Version.Null, strconv.Quote(check.Version.Text))
|
||||||
|
if check.Location == nil {
|
||||||
|
_, _ = writer.WriteString(" location: null\n")
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(writer, " location: {source_id: %s, line: %d, column: %d}\n",
|
||||||
|
strconv.Quote(check.Location.SourceID), check.Location.Line, check.Location.Column)
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(writer, " redacted: %t\n", check.Redacted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
134
integrations/ckconfig/sources_test.go
Normal file
134
integrations/ckconfig/sources_test.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package ckconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSourcesProjectNativeEvidenceInTextAndYAML(t *testing.T) {
|
||||||
|
servicePath, err := configkit.ParsePath("/services")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
otherPath, err := configkit.ParsePath("/other")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
versionPath, err := configkit.ParsePath("/version")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nullVersionPath, err := configkit.ParsePath("/metadata/version")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := &sourceInventory{values: []configkit.SourceInfo{
|
||||||
|
{
|
||||||
|
ID: "root", Label: "root source", Path: "root.yml",
|
||||||
|
PlacementMode: configkit.PlacementModeMapping,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "item", Label: "sequence item", Path: "item.yml",
|
||||||
|
Destination: servicePath, PlacementMode: configkit.PlacementModeSequenceItem,
|
||||||
|
DestinationUnresolved: true,
|
||||||
|
VersionCheck: &configkit.SourceVersionCheck{
|
||||||
|
Path: versionPath,
|
||||||
|
Version: configkit.Version{Present: true, Text: "v2"},
|
||||||
|
Location: &configkit.Location{SourceID: "item.yml", Line: 4, Column: 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "null", Label: "null version", Path: "null.yml",
|
||||||
|
Destination: otherPath, PlacementMode: configkit.PlacementModeMapping,
|
||||||
|
VersionCheck: &configkit.SourceVersionCheck{
|
||||||
|
Path: nullVersionPath,
|
||||||
|
Version: configkit.Version{Present: true, Null: true},
|
||||||
|
Redacted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{
|
||||||
|
{Name: "app", Operations: []Operation{Sources}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
return commandkit.Acquired[View]{Value: View{Sources: reader}}, nil
|
||||||
|
}},
|
||||||
|
}}))
|
||||||
|
|
||||||
|
var yamlOutput bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &yamlOutput, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || reader.calls != 1 {
|
||||||
|
t.Fatalf("YAML source report outcome=%#v calls=%d", outcome, reader.calls)
|
||||||
|
}
|
||||||
|
var report struct {
|
||||||
|
Scope string `yaml:"scope"`
|
||||||
|
Sources []struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Destination string `yaml:"destination"`
|
||||||
|
Placement string `yaml:"placement"`
|
||||||
|
DestinationUnresolved bool `yaml:"destination_unresolved"`
|
||||||
|
VersionCheck *struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Version struct {
|
||||||
|
Present bool `yaml:"present"`
|
||||||
|
Null bool `yaml:"null"`
|
||||||
|
Text string `yaml:"text"`
|
||||||
|
} `yaml:"version"`
|
||||||
|
Location *struct {
|
||||||
|
SourceID string `yaml:"source_id"`
|
||||||
|
Line int `yaml:"line"`
|
||||||
|
Column int `yaml:"column"`
|
||||||
|
} `yaml:"location"`
|
||||||
|
Redacted bool `yaml:"redacted"`
|
||||||
|
} `yaml:"version_check"`
|
||||||
|
} `yaml:"sources"`
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(yamlOutput.Bytes(), &report); err != nil {
|
||||||
|
t.Fatalf("decode source report: %v\n%s", err, yamlOutput.String())
|
||||||
|
}
|
||||||
|
if report.Scope != "app" || len(report.Sources) != 3 {
|
||||||
|
t.Fatalf("source report scope/order = %q, %#v", report.Scope, report.Sources)
|
||||||
|
}
|
||||||
|
if got := []string{report.Sources[0].ID, report.Sources[1].ID, report.Sources[2].ID}; !reflect.DeepEqual(got, []string{"root", "item", "null"}) {
|
||||||
|
t.Fatalf("source order = %v", got)
|
||||||
|
}
|
||||||
|
if root := report.Sources[0]; root.Destination != "" || root.Placement != "mapping" || root.DestinationUnresolved || root.VersionCheck != nil {
|
||||||
|
t.Errorf("root source projection = %#v", root)
|
||||||
|
}
|
||||||
|
item := report.Sources[1]
|
||||||
|
if item.Destination != "/services" || item.Placement != "sequence_item" || !item.DestinationUnresolved {
|
||||||
|
t.Errorf("sequence item projection = %#v", item)
|
||||||
|
}
|
||||||
|
if check := item.VersionCheck; check == nil || check.Path != "/version" || !check.Version.Present || check.Version.Null || check.Version.Text != "v2" || check.Location == nil || check.Location.SourceID != "item.yml" || check.Location.Line != 4 || check.Location.Column != 2 || check.Redacted {
|
||||||
|
t.Errorf("text version evidence projection = %#v", check)
|
||||||
|
}
|
||||||
|
nullSource := report.Sources[2]
|
||||||
|
if nullSource.Destination != "/other" || nullSource.Placement != "mapping" || nullSource.DestinationUnresolved {
|
||||||
|
t.Errorf("mapping projection = %#v", nullSource)
|
||||||
|
}
|
||||||
|
if check := nullSource.VersionCheck; check == nil || check.Path != "/metadata/version" || !check.Version.Present || !check.Version.Null || check.Version.Text != "" || check.Location != nil || !check.Redacted {
|
||||||
|
t.Errorf("null/redacted version evidence projection = %#v", check)
|
||||||
|
}
|
||||||
|
|
||||||
|
var textOutput bytes.Buffer
|
||||||
|
outcome = app.Execute(context.Background(), []string{"config", "sources", "--format", "text"}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || reader.calls != 2 {
|
||||||
|
t.Fatalf("text source report outcome=%#v calls=%d", outcome, reader.calls)
|
||||||
|
}
|
||||||
|
for _, fragment := range []string{
|
||||||
|
`id: "root"`, `destination: ""`, `placement: "mapping"`, "destination_unresolved: false", "version_check: null",
|
||||||
|
`destination: "/services"`, `placement: "sequence_item"`, "destination_unresolved: true", `path: "/version"`, `text: "v2"`,
|
||||||
|
`destination: "/other"`, `path: "/metadata/version"`, "null: true", `text: ""`, "location: null", "redacted: true",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(textOutput.String(), fragment) {
|
||||||
|
t.Errorf("text report missing %q:\n%s", fragment, textOutput.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user