Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f9de1db
Explain cost variance and root cause from the CLI: variance-engine
Sep 26, 2026
f601f06
Explain cost variance and root cause from the CLI: variance-engine
Sep 26, 2026
fb903cf
Explain cost variance and root cause from the CLI: variance-cli
Sep 26, 2026
fe9aa52
Explain cost variance and root cause from the CLI: variance-cli
Sep 26, 2026
52bde8d
Explain cost variance and root cause from the CLI: variance-cli
Sep 26, 2026
648b07b
Explain cost variance and root cause from the CLI: variance-engine
Sep 27, 2026
4944207
Explain cost variance and root cause from the CLI: variance-cli
Sep 27, 2026
ae0cdcc
Explain cost variance and root cause from the CLI: variance-cli
Sep 28, 2026
b1c9ae0
Explain cost variance and root cause from the CLI: variance-cli
Sep 28, 2026
f5a428c
Explain cost variance and root cause from the CLI: variance-cli
Sep 28, 2026
4d17343
Explain cost variance and root cause from the CLI: variance-engine
Sep 29, 2026
d8b11c7
Explain cost variance and root cause from the CLI: variance-cli
Sep 29, 2026
89e528a
Explain cost variance and root cause from the CLI: variance-cli
Sep 29, 2026
9a56442
style: format cost variance CLI implementation
Sep 29, 2026
b021e49
fix: validate variance percentages against measured totals
Sep 29, 2026
967681c
fix: reject unsupported driver evidence in explanations
Sep 29, 2026
4a475bd
Merge remote-tracking branch 'origin/main' into autonomous/dc767eb795…
Oct 4, 2026
8fa8ab4
fix: keep synthetic variance evidence explainable
Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions cmd/cost_diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package cmd

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"

costv1 "github.com/IntelIP/ProfitCtl/pkg/contracts/cost/v1"
variancev1 "github.com/IntelIP/ProfitCtl/pkg/variance/v1"
"github.com/spf13/cobra"
)

var costDiffCmd = newCostDiffCommand()

func init() { rootCmd.AddCommand(costDiffCmd) }

// Diff compares two observations without inventing driver evidence. Use Compare
// with a variancev1.Input when driver pairs, exchange rates or receipts exist.
func Diff(ctx context.Context, forecast, actual *costv1.CostObservation) (*variancev1.Result, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if forecast == nil {
return nil, errors.New("forecast observation is required")
}
in := variancev1.Input{Forecast: *forecast, Actual: actual}
if actual == nil {
in.UnavailableReason = "actual observation not supplied"
}
result, err := variancev1.Compare(in)
if err != nil {
return nil, err
}
return &result, nil
}

func newCostDiffCommand() *cobra.Command {
command := &cobra.Command{Use: "diff", Short: "Compare local forecast and actual costs", Args: cobra.NoArgs}
command.Flags().StringP("input", "i", "", "Variance input JSON (observations, driver pairs, exchange rates and receipts)")
command.Flags().String("forecast", "", "Forecast CostObservation JSON (without driver pairs)")
command.Flags().String("actual", "", "Actual CostObservation JSON (without driver pairs)")
command.Flags().StringP("output", "o", "", "Optional JSON output file (also prints to stdout)")
command.RunE = func(cmd *cobra.Command, _ []string) error {
input, _ := cmd.Flags().GetString("input")
forecastPath, _ := cmd.Flags().GetString("forecast")
actualPath, _ := cmd.Flags().GetString("actual")
output, _ := cmd.Flags().GetString("output")
if (input == "" && (forecastPath == "" || actualPath == "")) || (input != "" && (forecastPath != "" || actualPath != "")) {
return wrapExit(2, errors.New("provide --input or both --forecast and --actual"))
}
if err := rejectVarianceOutputAlias(output, input, forecastPath, actualPath); err != nil {
return wrapExit(2, err)
}
var in variancev1.Input
if input != "" {
if err := readVarianceJSON(input, &in); err != nil {
return wrapExit(2, fmt.Errorf("input: %w", err))
}
} else {
if err := readVarianceJSON(forecastPath, &in.Forecast); err != nil {
return wrapExit(2, fmt.Errorf("forecast: %w", err))
}
var actual costv1.CostObservation
if err := readVarianceJSON(actualPath, &actual); err != nil {
return wrapExit(2, fmt.Errorf("actual: %w", err))
}
in.Actual = &actual
}
if err := cmd.Context().Err(); err != nil {
return wrapExit(2, err)
}
report, err := variancev1.Compare(in)
if err != nil {
return wrapExit(2, fmt.Errorf("diff: %w", err))
}
return emitVarianceJSON(cmd, output, report)
}
return command
}

func readVarianceJSON(path string, target any) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
return strictVarianceJSON(data, target)
}

func strictVarianceJSON(data []byte, target any) error {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
if err := decoder.Decode(new(any)); !errors.Is(err, io.EOF) {
if err == nil {
return errors.New("multiple JSON values are not allowed")
}
return err
}
return nil
}

func rejectVarianceOutputAlias(output string, inputs ...string) error {
if strings.TrimSpace(output) == "" {
return nil
}
for _, input := range inputs {
if input == "" {
continue
}
alias, err := pathsAlias(output, input)
if err != nil {
return fmt.Errorf("compare output paths: %w", err)
}
if alias {
return fmt.Errorf("--output must not reference input %q", input)
}
}
return nil
}

func emitVarianceJSON(cmd *cobra.Command, output string, report any) error {
payload, err := json.MarshalIndent(report, "", " ")
if err != nil {
return wrapExit(3, fmt.Errorf("encode variance report: %w", err))
}
payload = append(payload, '\n')
if strings.TrimSpace(output) != "" {
if err := os.WriteFile(output, payload, 0600); err != nil {
return wrapExit(3, fmt.Errorf("write output: %w", err))
}
}
if _, err := cmd.OutOrStdout().Write(payload); err != nil {
return wrapExit(3, fmt.Errorf("write stdout: %w", err))
}
return nil
}
224 changes: 224 additions & 0 deletions cmd/cost_explain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package cmd

import (
"context"
"errors"
"fmt"
"math"
"sort"
"strings"
"time"

costv1 "github.com/IntelIP/ProfitCtl/pkg/contracts/cost/v1"
variancev1 "github.com/IntelIP/ProfitCtl/pkg/variance/v1"
"github.com/spf13/cobra"
)

// DriverContribution preserves the modeled decomposition and adds non-causal
// suggestions. Receipts and source references remain on the embedded contribution.
type DriverContribution struct {
variancev1.Contribution
RemedialActions []string `json:"remedial_actions"`
}

type ExplainReport struct {
SchemaVersion string `json:"schema_version"`
Status string `json:"status"`
Window costv1.TimeWindow `json:"window"`
ForecastObservationID string `json:"forecast_observation_id"`
ActualObservationID string `json:"actual_observation_id,omitempty"`
UnavailableReason string `json:"unavailable_reason,omitempty"`
Confidence costv1.Confidence `json:"confidence"`
ConfidenceScore float64 `json:"confidence_score"`
Forecast costv1.Money `json:"forecast"`
Actual *costv1.Money `json:"actual"`
VarianceAbsolute *costv1.Money `json:"variance_absolute"`
VariancePercent *float64 `json:"variance_percent"`
DriverContributions []DriverContribution `json:"driver_contributions"`
Residual *costv1.Money `json:"residual"`
MissingEvidence []string `json:"missing_evidence"`
Sources []costv1.SourceReference `json:"sources"`
}

// Explain ranks existing modeled evidence; it never asserts delivery causation.
func Explain(ctx context.Context, report *variancev1.Result) (*ExplainReport, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if report == nil || report.SchemaVersion != variancev1.SchemaVersion || (report.Status != "available" && report.Status != "unavailable") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the complete diff result before explaining it

When profitctl explain reads a hand-edited or corrupt diff file, this gate accepts any report with the right version and status if its arithmetic reconciles, then copies unchecked fields into the output. For example, blank observation IDs, an empty window, and empty money currencies can pass with a zero forecast and reconciled actual/variance/residual, but the emitted explanation is rejected by schemas/cost-variance/v1/schema.json (which requires non-empty IDs, RFC3339 window timestamps, and three-letter currencies). Validate the complete Result contract before constructing the report.

AGENTS.md reference: AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

return nil, errors.New("valid cost variance result is required")
}
for _, field := range []struct {
name string
money *costv1.Money
}{{"forecast", &report.Forecast}, {"actual", report.Actual}, {"absolute_variance", report.AbsoluteVariance}, {"residual", report.Residual}} {
if field.money != nil && (math.IsNaN(field.money.Amount) || math.IsInf(field.money.Amount, 0)) {
return nil, fmt.Errorf("%s.amount must be finite", field.name)
}
}
if report.PercentageVariance != nil && (math.IsNaN(*report.PercentageVariance) || math.IsInf(*report.PercentageVariance, 0)) {
return nil, errors.New("percentage_variance must be finite")
}
if report.Status == "unavailable" && report.PercentageVariance != nil {
return nil, errors.New("percentage_variance must be null when actual is unavailable")
}
if report.Status == "available" && (report.Actual == nil || report.AbsoluteVariance == nil || report.Residual == nil) {
return nil, errors.New("available result lacks actual, variance or residual")
}
if report.Status == "available" {
for _, field := range []struct {
name string
money *costv1.Money
}{{"actual", report.Actual}, {"absolute_variance", report.AbsoluteVariance}, {"residual", report.Residual}} {
if field.money.Currency != report.Forecast.Currency {
return nil, fmt.Errorf("%s currency %q differs from forecast currency %q", field.name, field.money.Currency, report.Forecast.Currency)
}
}
}
if report.Status == "available" {
delta := report.Actual.Amount - report.Forecast.Amount
if math.IsNaN(delta) || math.IsInf(delta, 0) || math.Abs(delta-report.AbsoluteVariance.Amount) > variancev1.Rounding {
return nil, errors.New("absolute_variance.amount must equal actual.amount minus forecast.amount within rounding tolerance")
}
if report.Forecast.Amount == 0 {
if report.PercentageVariance != nil {
return nil, errors.New("percentage_variance must be null when forecast.amount is zero")
}
} else {
expected := delta / report.Forecast.Amount * 100
if math.IsNaN(expected) || math.IsInf(expected, 0) {
return nil, errors.New("percentage_variance overflows for supplied amounts")
}
if report.PercentageVariance == nil || math.Abs(*report.PercentageVariance-expected) > variancev1.Rounding {
return nil, fmt.Errorf("percentage_variance must equal 100 * (actual.amount - forecast.amount) / forecast.amount (%.12g) within rounding tolerance", expected)
}
}
}
if report.Status == "unavailable" && (report.Actual != nil || report.AbsoluteVariance != nil || report.Residual != nil || len(report.Contributions) != 0) {
return nil, errors.New("unavailable result cannot contain measured variance")
}
if report.Status == "unavailable" {
if strings.TrimSpace(report.UnavailableReason) == "" {
return nil, errors.New("unavailable_reason is required when actual is unavailable")
}
visible := false
for _, evidence := range report.MissingEvidence {
detail := strings.TrimLeft(evidence, " \t\n\r")
if strings.HasPrefix(detail, "actual cost:") && strings.Contains(detail[len("actual cost:"):], report.UnavailableReason) {
visible = true
break
}
}
if !visible {
return nil, errors.New("missing_evidence must identify actual cost and name unavailable_reason when actual is unavailable")
}
}
out := &ExplainReport{SchemaVersion: report.SchemaVersion, Status: report.Status, Window: report.Window, ForecastObservationID: report.ForecastObservationID, ActualObservationID: report.ActualObservationID, UnavailableReason: report.UnavailableReason, Forecast: report.Forecast, Actual: report.Actual, VarianceAbsolute: report.AbsoluteVariance, VariancePercent: report.PercentageVariance, Residual: report.Residual, DriverContributions: []DriverContribution{}, MissingEvidence: report.MissingEvidence, Sources: report.Sources, Confidence: costv1.ConfidenceLow, ConfidenceScore: 0}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Give explain reports a distinct schema version

The diff result and explain report both emit profitctl.cost-variance/v1, but their required shapes are incompatible: Result uses absolute_variance, percentage_variance, and contributions, while ExplainReport uses variance_absolute, variance_percent, and driver_contributions. Because the explanation copies the result's version here, downstream consumers cannot route or validate a document by its declared schema version—each output is rejected by the other cost-variance/v1 schema. Give the explanation its own schema version/type, or retain a compatible envelope.

AGENTS.md reference: AGENTS.md:L20-L20

Useful? React with 👍 / 👎.

if out.MissingEvidence == nil {
out.MissingEvidence = []string{}
}
if report.Status == "available" {
out.ConfidenceScore = 1
// Result has sources but no total-cost claim confidence; provenance cannot establish it.
out.ConfidenceScore = .35
for _, name := range []string{"forecast", "actual"} {
out.MissingEvidence = append(out.MissingEvidence, name+" total cost: high-confidence claim evidence unavailable")
}
sum := report.Residual.Amount
for i, c := range report.Contributions {
if math.IsNaN(c.Amount.Amount) || math.IsInf(c.Amount.Amount, 0) {
return nil, fmt.Errorf("contributions[%d].amount.amount must be finite", i)
}
if c.Amount.Currency != report.Forecast.Currency || c.ConfidenceScore < 0 || c.ConfidenceScore > 1 || math.IsNaN(c.ConfidenceScore) || (c.Attribution != "unknown" && c.Attribution != "modeled") {
return nil, errors.New("invalid driver contribution")
}

for _, receipt := range c.Receipts {
_, timestampErr := time.Parse(time.RFC3339, receipt.Source.CapturedAt)
if timestampErr != nil {
_, timestampErr = time.Parse(time.DateOnly, receipt.Source.CapturedAt)
}
validKind := receipt.Kind == "commit" || receipt.Kind == "pull_request" || receipt.Kind == "release" || receipt.Kind == "deployment"
if receipt.DriverID != c.DriverID || receipt.ForecastObservationID != report.ForecastObservationID || receipt.ActualObservationID != report.ActualObservationID || report.ForecastObservationID == "" || report.ActualObservationID == "" || strings.TrimSpace(receipt.Source.ArtifactIdentity) == "" || receipt.Source.Type != costv1.SourceRuntimeLedger || timestampErr != nil || !validKind {
return nil, fmt.Errorf("contributions[%d] has an invalid exact receipt", i)
}
}
if (c.Kind == costv1.DriverCadence || c.Kind == costv1.DriverConcurrency || c.Kind == costv1.DriverUptime) && c.Attribution == "modeled" && len(c.Receipts) == 0 {
return nil, fmt.Errorf("contributions[%d] modeled attribution requires an exact receipt", i)
}
for _, source := range c.Sources {
if source.Type == costv1.SourceSyntheticFixture && (c.Confidence != costv1.ConfidenceLow || c.ConfidenceScore > .35) {
return nil, fmt.Errorf("contributions[%d] synthetic evidence requires low confidence", i)
}
}
sum += c.Amount.Amount
out.ConfidenceScore = math.Min(out.ConfidenceScore, c.ConfidenceScore)
action := "Review workload measurements and cost evidence before adjusting this driver"
switch c.Kind {
case costv1.DriverCadence:
action = "Review polling interval and measure command volume before changing cadence"
case costv1.DriverConcurrency:
action = "Review replica count and utilization before resizing"
case costv1.DriverVariable:
action = "Review request volume and per-unit price before tuning usage"
case costv1.DriverUptime:
action = "Review measured uptime and scheduling before changing runtime"
case costv1.DriverFixed:
action = "Review contracted unit price before renegotiating"
}
out.DriverContributions = append(out.DriverContributions, DriverContribution{Contribution: c, RemedialActions: []string{action}})
}
if math.Abs(sum-report.AbsoluteVariance.Amount) > variancev1.Rounding {
return nil, errors.New("driver contributions and residual do not reconcile")
}
sort.SliceStable(out.DriverContributions, func(i, j int) bool {
x, y := out.DriverContributions[i].Contribution, out.DriverContributions[j].Contribution
if math.Abs(x.Amount.Amount) == math.Abs(y.Amount.Amount) {
return x.DriverID < y.DriverID
}
return math.Abs(x.Amount.Amount) > math.Abs(y.Amount.Amount)
})
if math.Abs(report.Residual.Amount) > variancev1.Rounding || len(report.Contributions) == 0 {
out.ConfidenceScore = math.Min(out.ConfidenceScore, .35)
}
switch {
case out.ConfidenceScore >= .85:
out.Confidence = costv1.ConfidenceHigh
case out.ConfidenceScore >= .55:
out.Confidence = costv1.ConfidenceMedium
default:
out.Confidence = costv1.ConfidenceLow
}
}
return out, nil
}

var costExplainCmd = newCostExplainCommand()

func init() { rootCmd.AddCommand(costExplainCmd) }

func newCostExplainCommand() *cobra.Command {
command := &cobra.Command{Use: "explain", Short: "Rank evidenced cost variance drivers", Args: cobra.NoArgs}
command.Flags().StringP("input", "i", "", "Diff result JSON file")
command.Flags().StringP("output", "o", "", "Optional JSON output file (also prints to stdout)")
command.RunE = func(cmd *cobra.Command, _ []string) error {
input, _ := cmd.Flags().GetString("input")
output, _ := cmd.Flags().GetString("output")
if strings.TrimSpace(input) == "" {
return wrapExit(2, errors.New("--input is required"))
}
if err := rejectVarianceOutputAlias(output, input); err != nil {
return wrapExit(2, err)
}
var diff variancev1.Result
if err := readVarianceJSON(input, &diff); err != nil {
return wrapExit(2, fmt.Errorf("decode diff result: %w", err))
}
report, err := Explain(cmd.Context(), &diff)
if err != nil {
return wrapExit(2, fmt.Errorf("explain: %w", err))
}
return emitVarianceJSON(cmd, output, report)
}
return command
}
Loading
Loading