From 89a4096562bd7df1069be2d52d6b7258df96760a Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Fri, 31 Jul 2026 17:10:37 -0700 Subject: [PATCH 1/6] feat(cli): stabilize ProfitCtl package identity --- .github/workflows/ci.yml | 35 +++- Formula/profitctl.rb | 10 +- README.md | 15 +- cmd/commands.go | 2 +- cmd/doctor.go | 181 +++++++++++++++++ cmd/doctor_test.go | 82 ++++++++ cmd/profitctl/main.go | 20 ++ cmd/root.go | 20 +- cmd/version.go | 16 ++ cmd/version_test.go | 34 ++++ docs/INSTALL.md | 17 +- .../PCTL-3-PCTL-14-decision-packet.md | 192 ++++++++++++++++++ main.go | 1 + 13 files changed, 606 insertions(+), 19 deletions(-) create mode 100644 cmd/doctor.go create mode 100644 cmd/doctor_test.go create mode 100644 cmd/profitctl/main.go create mode 100644 cmd/version.go create mode 100644 cmd/version_test.go create mode 100644 docs/decisions/PCTL-3-PCTL-14-decision-packet.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65c896b..cb31679 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,14 @@ jobs: - name: Run tests run: go test ./... + - name: Verify Go developer fallback identity + run: | + GOBIN="${RUNNER_TEMP}/go-bin" + mkdir -p "${GOBIN}" + GOBIN="${GOBIN}" go install ./cmd/profitctl + test -x "${GOBIN}/profitctl" + test "$("${GOBIN}/profitctl" --version)" = "profitctl dev" + verify-install-smoke: runs-on: ubuntu-latest steps: @@ -71,13 +79,36 @@ jobs: - name: Run installer smoke test run: | INSTALL_DIR="${RUNNER_TEMP}/profitctl-bin" + TEST_HOME="${RUNNER_TEMP}/profitctl-home" mkdir -p "${INSTALL_DIR}" + mkdir -p "${TEST_HOME}" PROFITCTL_DOWNLOAD_BASE_URL="${PROFITCTL_TEST_MIRROR}" \ PROFITCTL_VERSION=v0.0.0-ci \ PROFITCTL_INSTALL_DIR="${INSTALL_DIR}" \ bash scripts/install.sh - "${INSTALL_DIR}/profitctl" --help >/dev/null - "${INSTALL_DIR}/profitctl" validate -f examples/mix_profit.yml >/dev/null + env -i \ + HOME="${TEST_HOME}" \ + PATH="${INSTALL_DIR}:/usr/bin:/bin" \ + CONFIG_FILE="${GITHUB_WORKSPACE}/examples/mix_profit.yml" \ + CATALOG_FILE="${GITHUB_WORKSPACE}/provider_catalog/ai_saas_defaults.yml" \ + sh -eu <<'EOF' + test "$(command -v profitctl)" = "${PATH%%:*}/profitctl" + test "$(profitctl --version)" = "profitctl v0.0.0-ci" + test "$(profitctl version)" = "profitctl v0.0.0-ci" + profitctl --help >/dev/null + profitctl validate -f "${CONFIG_FILE}" >/dev/null + profitctl doctor -f "${CONFIG_FILE}" --catalog "${CATALOG_FILE}" >/dev/null + + set +e + doctor_output="$(profitctl doctor -f "${HOME}/missing-profit.yml" --catalog "${HOME}/missing-catalog.yml" 2>&1)" + doctor_status=$? + set -e + test "${doctor_status}" -eq 2 + printf '%s\n' "${doctor_output}" | grep -F "[fail] config:" >/dev/null + printf '%s\n' "${doctor_output}" | grep -F "profitctl init --file" >/dev/null + printf '%s\n' "${doctor_output}" | grep -F "[fail] catalog:" >/dev/null + printf '%s\n' "${doctor_output}" | grep -F "no fallback or local state change was performed" >/dev/null + EOF security-scan: runs-on: ubuntu-latest diff --git a/Formula/profitctl.rb b/Formula/profitctl.rb index a36c41e..67b6061 100644 --- a/Formula/profitctl.rb +++ b/Formula/profitctl.rb @@ -1,26 +1,26 @@ class Profitctl < Formula desc "CLI for profit-first unit economics simulations" homepage "https://github.com/IntelIP/ProfitCtl" - version "0.1.3" + version "0.2.0" license "MIT" on_macos do if Hardware::CPU.arm? url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_darwin_arm64.tar.gz" - sha256 "fa4734c0ef5e2111f6d2a7b024ec10d01675fd27e56c932bd14fcec61ec16999" + sha256 "6476dac94d391f40ea21c4555fce1c5814fe93bb4c4b35a8ea2788dece5041c5" else url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_darwin_amd64.tar.gz" - sha256 "287e883308c2793fb12ff9e3c9d9c09bce69522925555e034bfdd31805295f8c" + sha256 "b36d0fe67c9f869b630e3e68103ed8ad4cb49ca9f03e197fac3d4478e6fd4f3a" end end on_linux do if Hardware::CPU.arm? url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_linux_arm64.tar.gz" - sha256 "aeb904b5fe0b9e124a8073e6ea542852f7aefcf423fff6896965bd33f7365fd3" + sha256 "b847a1f690bac63dd4ab3b14fa47bfcc365719f6d962e785ce4f0aa27bd320f0" else url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_linux_amd64.tar.gz" - sha256 "4d3ecef622d3876665c8cc20a30b4dcb8515c3abc13ebe7014945fa9172fa7f8" + sha256 "a1acd22aedf1726faba6561347d871cfe63937905b2dc714769aa0ab1aca6b00" end end diff --git a/README.md b/README.md index a7289f0..bb0b77a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ It helps teams model fixed + variable costs, simulate growth and stress scenario - `calibrate`: normalize YAML, JSON, or CSV calibration exports into ProfitCtl calibration artifacts - `validate`: validate config structure and rules - `detect`: scan repository config files and return JSON service/dependency analysis +- `doctor`: check local binary, runtime, config, and catalog readiness without changing local state - pricing modes: `tiered`, `mix`, and `hybrid` - workspace-aware pricing and minimum-floor scenario modeling - payment-fee modeling with monthly vs annual billing mix @@ -33,8 +34,11 @@ It helps teams model fixed + variable costs, simulate growth and stress scenario ```bash curl -fsSL https://raw.githubusercontent.com/IntelIP/ProfitCtl/main/scripts/install.sh | bash +profitctl --version ``` +The installer downloads a GitHub Release archive, verifies it against that release's `SHA256SUMS`, and installs `profitctl` to `~/.local/bin` unless a custom prefix is selected. + ### Homebrew ```bash @@ -44,21 +48,24 @@ brew install profitctl See [Install Guide](docs/INSTALL.md) for pinned versions, Homebrew, custom prefixes, and source-based installs. -### Build from source +### Developer build from source ```bash git clone https://github.com/IntelIP/ProfitCtl.git cd ProfitCtl go build -o profitctl . -./profitctl --help +./profitctl --version +./profitctl doctor -f examples/valid_profit.yml --catalog provider_catalog/ai_saas_defaults.yml ``` -### Go install +### Developer install with Go ```bash -go install github.com/IntelIP/ProfitCtl@latest +go install github.com/IntelIP/ProfitCtl/cmd/profitctl@ ``` +Go-based paths are explicit developer fallbacks. GitHub Release installation is the supported user path; Homebrew is secondary. + ## Quick Usage ```bash diff --git a/cmd/commands.go b/cmd/commands.go index 12f728b..afb60bf 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -5,5 +5,5 @@ import "github.com/spf13/cobra" func AddCommands(root *cobra.Command) { var cfgFile string root.PersistentFlags().StringVarP(&cfgFile, "file", "f", "profit.yml", "Configuration file path") - root.AddCommand(initCmd, simulateCmd, compareCmd, validateCmd, calibrateCmd, detectCmd) + root.AddCommand(initCmd, simulateCmd, compareCmd, validateCmd, calibrateCmd, detectCmd, versionCmd, doctorCmd) } diff --git a/cmd/doctor.go b/cmd/doctor.go new file mode 100644 index 0000000..e48aa5c --- /dev/null +++ b/cmd/doctor.go @@ -0,0 +1,181 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "runtime" + "strings" + "time" + + "github.com/IntelIP/ProfitCtl/internal/config" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +const defaultCatalogPath = "provider_catalog/ai_saas_defaults.yml" + +type catalogProbe struct { + CatalogVersion string `yaml:"catalog_version"` + Status string `yaml:"status"` + Entries []struct { + ID string `yaml:"id"` + Provider string `yaml:"provider"` + Service string `yaml:"service"` + Unit string `yaml:"unit"` + } `yaml:"entries"` +} + +var doctorCatalog string + +var doctorCmd = &cobra.Command{ + Use: "doctor", + Short: "Check local profitctl prerequisites", + Long: "Check binary identity, runtime support, configuration, and provider catalog readiness without changing local state.", + Args: cobra.NoArgs, + RunE: runDoctor, +} + +func init() { + doctorCmd.Flags().StringVar(&doctorCatalog, "catalog", defaultCatalogPath, "Provider catalog file path") +} + +func runDoctor(cmd *cobra.Command, args []string) error { + cfgFile, err := cmd.Flags().GetString("file") + if err != nil { + return wrapExit(2, fmt.Errorf("read --file: %w", err)) + } + if strings.TrimSpace(cfgFile) == "" { + cfgFile = "profit.yml" + } + + failures := 0 + warnings := 0 + report := func(status, name, detail string) { + switch status { + case "fail": + failures++ + case "warn": + warnings++ + } + fmt.Fprintf(cmd.OutOrStdout(), "[%s] %s: %s\n", status, name, detail) + } + + executable, executableErr := os.Executable() + if executableErr != nil { + report("fail", "binary", executableDetail(executable, executableErr)) + } else { + report("ok", "binary", executable) + } + + versionDetail := buildVersion + if buildVersion == "dev" { + versionDetail += " (developer build; release builds inject the package tag)" + } + if strings.TrimSpace(buildVersion) == "" { + report("fail", "version", "missing build identity") + } else if buildVersion == "dev" { + report("warn", "version", versionDetail) + } else { + report("ok", "version", versionDetail) + } + + runtimeDetail := runtime.GOOS + "/" + runtime.GOARCH + if !supportedRuntime(runtime.GOOS, runtime.GOARCH) { + runtimeDetail += " is not in the supported release matrix" + report("fail", "runtime", runtimeDetail) + } else { + report("ok", "runtime", runtimeDetail) + } + + _, configErr := config.ParseConfig(cfgFile) + if configErr != nil { + report("fail", "config", fmt.Sprintf("%s: %v; create or select one with `profitctl init --file %s`", cfgFile, configErr, cfgFile)) + } else { + report("ok", "config", cfgFile) + } + + catalog, catalogErr := readCatalog(doctorCatalog) + if catalogErr != nil { + report("fail", "catalog", fmt.Sprintf("%s: %v; pass a readable catalog with `--catalog PATH`", doctorCatalog, catalogErr)) + } else { + fresh, freshness := catalogFreshness(catalog.CatalogVersion, time.Now()) + detail := fmt.Sprintf("%s (%s, %d entries; %s)", doctorCatalog, catalog.CatalogVersion, len(catalog.Entries), freshness) + if fresh { + report("ok", "catalog", detail) + } else { + report("warn", "catalog", detail+"; stale catalog cannot support current-price claims") + } + } + + if failures > 0 { + fmt.Fprintf(cmd.OutOrStdout(), "doctor found %d failed check(s); no fallback or local state change was performed\n", failures) + return wrapExitSilent(2, fmt.Errorf("doctor found %d failed check(s)", failures)) + } + + if warnings > 0 { + fmt.Fprintf(cmd.OutOrStdout(), "doctor passed with %d warning(s)\n", warnings) + } else { + fmt.Fprintln(cmd.OutOrStdout(), "doctor passed") + } + return nil +} + +func executableDetail(path string, err error) string { + if err != nil { + return err.Error() + } + return path +} + +func supportedRuntime(goos, goarch string) bool { + switch goos + "/" + goarch { + case "linux/amd64", "linux/arm64", "darwin/amd64", "darwin/arm64", "windows/amd64": + return true + default: + return false + } +} + +func catalogFreshness(catalogVersion string, now time.Time) (bool, string) { + capturedAt, err := time.Parse("2006-01-02", catalogVersion) + if err != nil { + return false, "capture date is not YYYY-MM-DD" + } + ageDays := int(now.UTC().Sub(capturedAt).Hours() / 24) + if ageDays < 0 { + return false, "capture date is in the future" + } + if ageDays > 30 { + return false, fmt.Sprintf("%d days old; 30-day freshness window exceeded", ageDays) + } + return true, fmt.Sprintf("%d days old", ageDays) +} + +func readCatalog(path string) (*catalogProbe, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var catalog catalogProbe + if err := yaml.Unmarshal(data, &catalog); err != nil { + return nil, fmt.Errorf("parse YAML: %w", err) + } + if strings.TrimSpace(catalog.CatalogVersion) == "" { + return nil, errors.New("catalog_version is required") + } + if strings.TrimSpace(catalog.Status) == "" { + return nil, errors.New("status is required") + } + if len(catalog.Entries) == 0 { + return nil, errors.New("at least one entry is required") + } + for i, entry := range catalog.Entries { + if strings.TrimSpace(entry.ID) == "" || strings.TrimSpace(entry.Provider) == "" || + strings.TrimSpace(entry.Service) == "" || strings.TrimSpace(entry.Unit) == "" { + return nil, fmt.Errorf("entry %d requires id, provider, service, and unit", i+1) + } + } + return &catalog, nil +} diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go new file mode 100644 index 0000000..5433fd6 --- /dev/null +++ b/cmd/doctor_test.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunDoctorPassesWithValidLocalInputs(t *testing.T) { + oldCatalog := doctorCatalog + defer func() { doctorCatalog = oldCatalog }() + doctorCatalog = filepath.Join("..", "provider_catalog", "ai_saas_defaults.yml") + + command := doctorTestCommand(filepath.Join("..", "examples", "valid_profit.yml")) + var output bytes.Buffer + command.SetOut(&output) + + err := runDoctor(command, nil) + require.NoError(t, err) + assert.Contains(t, output.String(), "[ok] binary:") + assert.Contains(t, output.String(), "[ok] config:") + assert.Contains(t, output.String(), "[warn] catalog:") + assert.Contains(t, output.String(), "stale catalog cannot support current-price claims") + assert.Contains(t, output.String(), "doctor passed") +} + +func TestRunDoctorReportsMissingInputsWithoutFallback(t *testing.T) { + oldCatalog := doctorCatalog + defer func() { doctorCatalog = oldCatalog }() + doctorCatalog = filepath.Join(t.TempDir(), "missing-catalog.yml") + + command := doctorTestCommand(filepath.Join(t.TempDir(), "missing-profit.yml")) + var output bytes.Buffer + command.SetOut(&output) + + err := runDoctor(command, nil) + require.Error(t, err) + var exitErr *ExitError + require.True(t, errors.As(err, &exitErr)) + assert.Equal(t, 2, exitErr.Code) + assert.True(t, exitErr.Silent) + assert.Contains(t, output.String(), "[fail] config:") + assert.Contains(t, output.String(), "profitctl init --file") + assert.Contains(t, output.String(), "[fail] catalog:") + assert.Contains(t, output.String(), "--catalog PATH") + assert.Contains(t, output.String(), "no fallback or local state change was performed") +} + +func TestReadCatalogRejectsIncompleteEntry(t *testing.T) { + path := filepath.Join(t.TempDir(), "catalog.yml") + err := os.WriteFile(path, []byte("catalog_version: today\nstatus: test\nentries:\n - id: incomplete\n"), 0644) + require.NoError(t, err) + + _, err = readCatalog(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "requires id, provider, service, and unit") +} + +func TestCatalogFreshness(t *testing.T) { + now := time.Date(2026, time.July, 31, 0, 0, 0, 0, time.UTC) + + fresh, detail := catalogFreshness("2026-07-15", now) + assert.True(t, fresh) + assert.Contains(t, detail, "16 days old") + + fresh, detail = catalogFreshness("2026-05-29", now) + assert.False(t, fresh) + assert.Contains(t, detail, "30-day freshness window exceeded") +} + +func doctorTestCommand(configPath string) *cobra.Command { + command := &cobra.Command{} + command.Flags().String("file", configPath, "") + return command +} diff --git a/cmd/profitctl/main.go b/cmd/profitctl/main.go new file mode 100644 index 0000000..af5b2a7 --- /dev/null +++ b/cmd/profitctl/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "os" + + "github.com/IntelIP/ProfitCtl/cmd" +) + +var version = "dev" + +func main() { + cmd.SetVersion(version) + if err := cmd.Execute(); err != nil { + if cmd.ShouldPrintError(err) { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(cmd.ExitCode(err)) + } +} diff --git a/cmd/root.go b/cmd/root.go index d523309..9e20468 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,11 +1,18 @@ package cmd -import "github.com/spf13/cobra" +import ( + "strings" + + "github.com/spf13/cobra" +) + +var buildVersion = "dev" var rootCmd = &cobra.Command{ Use: "profitctl", Short: "Profit-first unit economics as code", Long: "profitctl simulates profitability, validates covenant constraints, and detects cost-related services from repository configuration.", + Version: buildVersion, SilenceUsage: true, SilenceErrors: true, } @@ -15,6 +22,17 @@ func Execute() error { return rootCmd.Execute() } +// SetVersion supplies the build-time version used by --version, version, and doctor. +func SetVersion(version string) { + version = strings.TrimSpace(version) + if version == "" { + version = "dev" + } + buildVersion = version + rootCmd.Version = version +} + func init() { + rootCmd.SetVersionTemplate("{{.Name}} {{.Version}}\n") AddCommands(rootCmd) } diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..5657f23 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the profitctl version", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(cmd.OutOrStdout(), "profitctl %s\n", buildVersion) + }, +} diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 0000000..ca76538 --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetVersion(t *testing.T) { + oldVersion := buildVersion + defer SetVersion(oldVersion) + + SetVersion("v1.2.3") + assert.Equal(t, "v1.2.3", buildVersion) + assert.Equal(t, "v1.2.3", rootCmd.Version) + + SetVersion(" ") + assert.Equal(t, "dev", buildVersion) + assert.Equal(t, "dev", rootCmd.Version) +} + +func TestVersionCommand(t *testing.T) { + oldVersion := buildVersion + defer SetVersion(oldVersion) + SetVersion("v1.2.3") + + var output bytes.Buffer + versionCmd.SetOut(&output) + defer versionCmd.SetOut(nil) + + versionCmd.Run(versionCmd, nil) + assert.Equal(t, "profitctl v1.2.3\n", output.String()) +} diff --git a/docs/INSTALL.md b/docs/INSTALL.md index d96b008..3d1d497 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -19,7 +19,7 @@ The script installs to `~/.local/bin` by default. If that directory is not on yo ## Pin a Version ```bash -curl -fsSL https://raw.githubusercontent.com/IntelIP/ProfitCtl/main/scripts/install.sh | PROFITCTL_VERSION=v0.1.2 bash +curl -fsSL https://raw.githubusercontent.com/IntelIP/ProfitCtl/main/scripts/install.sh | PROFITCTL_VERSION= bash ``` ## Homebrew @@ -48,28 +48,33 @@ curl -fsSL https://raw.githubusercontent.com/IntelIP/ProfitCtl/main/scripts/inst PROFITCTL_DOWNLOAD_BASE_URL=https://downloads.intelip.co/profitctl bash ``` -## Install from Source +## Developer Build from Source ```bash git clone https://github.com/IntelIP/ProfitCtl.git cd ProfitCtl go build -o profitctl . -./profitctl --help +./profitctl --version ``` -## Install via go install +## Developer Install via go install ```bash -go install github.com/IntelIP/ProfitCtl@latest +go install github.com/IntelIP/ProfitCtl/cmd/profitctl@ ``` +Source and Go installs are explicit developer fallbacks. The checksum-verified GitHub Release installer is the primary supported path; Homebrew is secondary. + ## Verify ```bash +profitctl --version profitctl --help -profitctl validate -f examples/mix_profit.yml +profitctl doctor -f /path/to/profit.yml --catalog /path/to/provider-catalog.yml ``` +`doctor` checks the current executable identity, injected version, supported runtime, selected config, and selected provider catalog. Missing required inputs produce an actionable message and exit code `2`; the command does not select another binary, create config, or mutate machine-global state. + ## Verify Release Integrity Public GitHub releases include: diff --git a/docs/decisions/PCTL-3-PCTL-14-decision-packet.md b/docs/decisions/PCTL-3-PCTL-14-decision-packet.md new file mode 100644 index 0000000..4e1438e --- /dev/null +++ b/docs/decisions/PCTL-3-PCTL-14-decision-packet.md @@ -0,0 +1,192 @@ +# PCTL-3 / PCTL-14 Owner Decision Packet + +Status: accepted by Hudson in Codex task `019fb11b-6f69-7d41-aabb-c51588d50989` + +Prepared: 2026-07-31 + +Execution authority: Plane PCTL-3, PCTL-14, and PCTL-2 + +Implementation authority: PCTL-2 only; merge, release, deployment, live provider access, spend, and secrets remain excluded + +## Decision Accepted + +The accepted contract: + +1. ProfitCtl becomes a local-first cost-intelligence control loop for founder-led developers and operators of AI-native software. +2. Existing simulation remains the forecast engine. First vertical slice adds synthetic Upstash idle-polling observations, variance attribution, and one bounded recommendation. +3. Provider catalog values remain sourced planning defaults, never guaranteed current prices. +4. Supported package identity is repository and Go module `github.com/IntelIP/ProfitCtl`, binary `profitctl`, GitHub Release install script as primary distribution, Homebrew as secondary distribution, and `go run` / `go install` as explicit development fallbacks. + +This acceptance clears product and provenance definition. It authorizes PCTL-2 implementation without selecting, publishing, merging, or deploying a release. + +## Known Facts + +- `main` presents ProfitCtl as a local CLI for pricing, unit economics, and recurring-margin simulation. +- Plane PCTL-3 and the proposed portfolio roadmap define a broader control loop: forecast, actual, variance, attribution, and action, with Upstash idle polling as first slice. +- Dirty canonical branch `docs/profitctl-oss-company-plan` proposes a different public center: “unit economics as code for AI-native software teams,” agent-decision demos, and a 120-day adoption plan. PCTL-10 and PCTL-16 preserve its work, but no active task, PR, or accepted product decision owns that direction. +- Canonical checkout is modified across product, open-core, growth, legal, standards-judge, skill, and website surfaces. It remains read-only. +- Repository identity is already `github.com/IntelIP/ProfitCtl`; installed binary name is already `profitctl`. +- Latest GitHub Release is `v0.2.0` from 2026-05-29. Repository Homebrew formula says `0.1.3`; install docs pin `v0.1.2`. +- Release builds inject `main.version`, but current CLI does not expose it. A clean local build rejects both `profitctl --version` and `profitctl doctor`. +- Seed provider catalog was captured 2026-05-29. Entries have type, confidence, capture date, and notes, but no refresh owner, cadence, expiry behavior, or Upstash entry. +- PCTL-17 validation manifest exists only on local branch `codex/pctl-17-product-validation`; it requires deterministic, zero-provider-call schema, semantic, workflow, operational, and security validation. + +## Canonical Dirty Ownership Inventory + +Read-only snapshot at base SHA `cd2e095f0dd02da4a9fdbb060adaed543cfea636`, branch `docs/profitctl-oss-company-plan`: + +- Product/open-core docs: `README.md`, `docs/DOCS_INDEX.md`, `docs/OPEN_CORE_PACKAGING.md`, `docs/OPEN_CORE_ROADMAP.md`, `docs/OPEN_SOURCE_COMPANY_PLAN.md`. +- Growth/design-partner work: `.github/ISSUE_TEMPLATE/design_partner_request.yml`, `docs/growth/README.md`, `docs/growth/benchmark-outreach-pack.md`, `docs/growth/benchmark-positioning.md`, `docs/growth/design-partner-offer.md`, `docs/growth/source-of-truth-guidance.md`, `docs/growth/value-proposition.md`, `docs/growth/agent-cost-os-demo-playbook.md`. +- Legal/project policy: `.gitignore`, `LICENSE`, `NOTICE`, `TRADEMARKS.md`. +- Standards and skill work: `docs/cost-model-standards.md`, `scripts/judge_cost_standards.go`, `scripts/judge_cost_standards_test.go`, `test/fixtures/agent_recommendation_valid.md`, `skills/profitctl-cost-aware/SKILL.md`. +- Local tooling/site surfaces: `.codex/`, `.entire/`, `website/`. + +All listed paths belong to the canonical company-plan work and remain untouched. PCTL-2 changes overlap only `README.md`, limited to supported install and CLI identity text on this isolated branch. No canonical content was copied or reconciled. + +## Option A — Control Loop with Simulation Core + +Recommended. + +Primary user: founder-led developer or operator responsible for cost and margin decisions in AI-native software. + +Job: forecast a change, ingest local or synthetic observations, explain variance, and choose a bounded engineering action before cost or margin drift becomes operational debt. + +First slice: + +1. model expected Upstash idle-polling cost; +2. ingest deterministic synthetic observations into a local ledger; +3. compare forecast with observations over an explicit time window; +4. attribute variance to polling volume and unit rate; +5. recommend one bounded change with confidence and provenance. + +Benefits: + +- preserves current simulation investment; +- gives PCTL-18, PCTL-19, PCTL-20, and PCTL-21 one coherent dependency chain; +- proves cost intelligence without live provider access; +- keeps install and exact-head validation foundational. + +Tradeoff: broader than current public CLI story. Requires explicit acceptance before implementation. + +## Option B — Simulation and Decision Advisor Only + +Keep current public product boundary. Ship stable CLI identity and agent-facing comparisons. Defer local ledger, observation, variance, and Upstash work. + +Benefits: smallest product change; closest to committed docs and current runtime. + +Tradeoff: invalidates current Definition-to-MVP Plane sequence and leaves “cost intelligence” as planning language rather than an executable control loop. + +## Option C — Agent Cost OS Public Wedge First + +Adopt dirty company-plan direction: AI-native cost-aware recommendations, public demos, and design-partner adoption before local ledger work. + +Benefits: strongest distribution narrative; uses existing dirty branch work. + +Tradeoff: collides with unowned canonical changes, defers Upstash control-loop proof, and risks treating proposed customer/GTM claims as accepted evidence. + +## Recommended Product Contract + +Product goal: help one technical operator turn explicit workload assumptions and local evidence into a trustworthy cost decision. + +Core workflow: + +```text +forecast -> observe -> normalize -> compare -> attribute -> recommend +``` + +Capability boundaries: + +- Forecast: deterministic scenario engine using explicit workload and rate assumptions. +- Observation: local, synthetic, imported telemetry, runtime ledger, or billing evidence. +- Normalization: units, currency, provider, service, region/tier, and time window. +- Variance: forecast versus observed cost for the same normalized boundary. +- Attribution: named workload, unit-rate, or fixed-cost drivers; no causal claim beyond evidence. +- Policy: visible covenants and thresholds; no autonomous infrastructure or billing mutation. +- Skill adoption: agents may construct scenarios and explain results; ProfitCtl owns deterministic contracts and calculations. + +Measurable MVP outcome: fresh user installs `profitctl`, ingests synthetic Upstash observations, explains one variance, and receives one bounded recommendation without editing code or contacting a live provider. + +Non-goals: + +- finance or FinOps system of record; +- provider-pricing oracle; +- live provider query or authenticated billing access; +- autonomous remediation; +- hosted SaaS, customer evidence, publication, release, merge, or deployment. + +## Recommended Provenance Policy + +### Authority by Claim + +| Claim | Preferred evidence | Default confidence | +| --- | --- | --- | +| Published unit rate | Official provider page or API, stored as `provider_catalog` with URL and capture time | medium | +| Billed amount | Authenticated billing export or invoice | high | +| Usage quantity | Product telemetry or runtime ledger | high when measured and scoped | +| Business assumption | Explicit `user_supplied` value | medium; high only when owner-confirmed | +| Planning fallback | Versioned template | low | + +No source type is universally “most authoritative.” Authority depends on claim. Invoice proves billed amount, not future rate. Telemetry proves observed usage, not provider pricing. Official pricing proves published rate, not the user’s realized bill. + +### Required Entry Fields + +- provider, service, region/tier where applicable; +- value, currency, unit, and applicable time window; +- `source.type`, source URL or artifact identity, `captured_at`, and effective date when known; +- confidence and confidence rationale; +- refresh owner, cadence, and stale-after date; +- note identifying exclusions, discounts, taxes, minimums, or unresolved ambiguity. + +### Refresh and Stale Behavior + +- Official pricing / provider catalog: refresh every 30 days; recheck within 7 days before a decision-grade release claim; refresh immediately after a known provider change. +- Templates: review every 90 days or at each ProfitCtl release, whichever comes first. +- Billing exports / invoices: refresh per import or billing close; never silently carry forward as a current rate. +- Telemetry / runtime ledgers: retain observation window and collector identity; never silently substitute for missing rate data. +- Stale planning data: remain usable only with a visible stale warning and downgraded confidence. +- Stale data blocks high-confidence, “current price,” or release-readiness claims. +- Refresh is controlled and maintainer-owned. No random scraping, background live query, spend, or credential use. + +### Upstash Demonstration + +PCTL-20 should add a source-backed Upstash catalog entry only after policy acceptance. Demo uses official pricing documentation plus deterministic synthetic polling observations. It must show source, age, confidence, owner, cadence, and stale behavior. No live Upstash account or query. + +## Recommended Package and Release Path for PCTL-2 + +- Canonical identity: `github.com/IntelIP/ProfitCtl`. +- Supported executable: `profitctl`. +- Primary install: checksum-verified GitHub Release install script into a user-selected or temporary prefix. +- Secondary install: Homebrew formula pinned to the same release. +- Development fallback: explicit `go run .` or `go install github.com/IntelIP/ProfitCtl/cmd/profitctl@`; never silent. The command package path is required because installing the mixed-case module root would create a `ProfitCtl` binary instead of the supported `profitctl` identity. +- Version behavior: `profitctl version` and `profitctl --version` report injected package tag; development builds report an explicit development identity. +- Doctor behavior: inspect current executable identity, configuration, provider catalog presence/age, and command prerequisites; explain failures; exit nonzero on missing required dependency; never switch binaries or mutate machine-global installation. +- Release number, publication, merge, and deployment remain separate decisions. + +## Acceptance Effect + +If accepted: + +- PCTL-3 product decision becomes authoritative. +- PCTL-14 provenance policy becomes authoritative. +- PCTL-3 may unblock PCTL-2 and PCTL-18 according to native dependencies. +- PCTL-14 may unblock PCTL-19 according to native dependencies. +- PCTL-2 may start in an isolated `codex/` branch and this worktree. + +If amended: record exact changed clauses before implementation. + +If rejected: choose Option B or C and re-sequence Plane dependencies before implementation. + +## Acceptance Record + +Hudson accepted Option A in full on 2026-07-31: + +- ProfitCtl is a local-first cost-intelligence control loop. +- Existing simulation remains the forecast engine. +- Synthetic Upstash idle polling is the first forecast → observation → variance → recommendation slice. +- Official pricing is medium-confidence catalog data; invoices own billed amounts; telemetry and runtime ledgers own usage. +- Stale data is warned and downgraded, and cannot support current-price claims. +- Canonical package/repository is `github.com/IntelIP/ProfitCtl`; binary is `profitctl`. +- Checksum-verified GitHub Release installer is primary; Homebrew is secondary; Go paths are explicit developer fallbacks. +- `--version` and `doctor` are required. + +Plane PCTL-3 and PCTL-14 record the accepted outcome and evidence contracts. PCTL-2 implementation proceeds on isolated branch `codex/pctl-2-stable-cli-identity`; the dirty canonical company-plan checkout remains separate and read-only. diff --git a/main.go b/main.go index d7805ca..af5b2a7 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( var version = "dev" func main() { + cmd.SetVersion(version) if err := cmd.Execute(); err != nil { if cmd.ShouldPrintError(err) { fmt.Fprintln(os.Stderr, err) From 202a0a28c34c3cca96423666b79644bc1d4945b1 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Fri, 31 Jul 2026 17:28:03 -0700 Subject: [PATCH 2/6] fix(cli): enforce package readiness contracts --- .github/workflows/ci.yml | 2 +- Formula/profitctl.rb | 10 +-- README.md | 2 +- cmd/doctor.go | 102 +++++++++++++++++------ cmd/doctor_test.go | 43 ++++++++-- test/fixtures/provider_catalog_valid.yml | 19 +++++ 6 files changed, 138 insertions(+), 40 deletions(-) create mode 100644 test/fixtures/provider_catalog_valid.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb31679..214aa21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: HOME="${TEST_HOME}" \ PATH="${INSTALL_DIR}:/usr/bin:/bin" \ CONFIG_FILE="${GITHUB_WORKSPACE}/examples/mix_profit.yml" \ - CATALOG_FILE="${GITHUB_WORKSPACE}/provider_catalog/ai_saas_defaults.yml" \ + CATALOG_FILE="${GITHUB_WORKSPACE}/test/fixtures/provider_catalog_valid.yml" \ sh -eu <<'EOF' test "$(command -v profitctl)" = "${PATH%%:*}/profitctl" test "$(profitctl --version)" = "profitctl v0.0.0-ci" diff --git a/Formula/profitctl.rb b/Formula/profitctl.rb index 67b6061..a36c41e 100644 --- a/Formula/profitctl.rb +++ b/Formula/profitctl.rb @@ -1,26 +1,26 @@ class Profitctl < Formula desc "CLI for profit-first unit economics simulations" homepage "https://github.com/IntelIP/ProfitCtl" - version "0.2.0" + version "0.1.3" license "MIT" on_macos do if Hardware::CPU.arm? url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_darwin_arm64.tar.gz" - sha256 "6476dac94d391f40ea21c4555fce1c5814fe93bb4c4b35a8ea2788dece5041c5" + sha256 "fa4734c0ef5e2111f6d2a7b024ec10d01675fd27e56c932bd14fcec61ec16999" else url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_darwin_amd64.tar.gz" - sha256 "b36d0fe67c9f869b630e3e68103ed8ad4cb49ca9f03e197fac3d4478e6fd4f3a" + sha256 "287e883308c2793fb12ff9e3c9d9c09bce69522925555e034bfdd31805295f8c" end end on_linux do if Hardware::CPU.arm? url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_linux_arm64.tar.gz" - sha256 "b847a1f690bac63dd4ab3b14fa47bfcc365719f6d962e785ce4f0aa27bd320f0" + sha256 "aeb904b5fe0b9e124a8073e6ea542852f7aefcf423fff6896965bd33f7365fd3" else url "https://github.com/IntelIP/ProfitCtl/releases/download/v#{version}/profitctl_v#{version}_linux_amd64.tar.gz" - sha256 "a1acd22aedf1726faba6561347d871cfe63937905b2dc714769aa0ab1aca6b00" + sha256 "4d3ecef622d3876665c8cc20a30b4dcb8515c3abc13ebe7014945fa9172fa7f8" end end diff --git a/README.md b/README.md index bb0b77a..11a9d5d 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ git clone https://github.com/IntelIP/ProfitCtl.git cd ProfitCtl go build -o profitctl . ./profitctl --version -./profitctl doctor -f examples/valid_profit.yml --catalog provider_catalog/ai_saas_defaults.yml +./profitctl doctor -f examples/valid_profit.yml --catalog /path/to/provider-catalog.yml ``` ### Developer install with Go diff --git a/cmd/doctor.go b/cmd/doctor.go index e48aa5c..404a429 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "runtime" "strings" "time" @@ -13,16 +14,28 @@ import ( "gopkg.in/yaml.v3" ) -const defaultCatalogPath = "provider_catalog/ai_saas_defaults.yml" - type catalogProbe struct { CatalogVersion string `yaml:"catalog_version"` Status string `yaml:"status"` Entries []struct { - ID string `yaml:"id"` - Provider string `yaml:"provider"` - Service string `yaml:"service"` - Unit string `yaml:"unit"` + ID string `yaml:"id"` + Provider string `yaml:"provider"` + Service string `yaml:"service"` + Unit string `yaml:"unit"` + Value *float64 `yaml:"value"` + Currency string `yaml:"currency"` + ConfidenceRationale string `yaml:"confidence_rationale"` + RefreshOwner string `yaml:"refresh_owner"` + RefreshCadence string `yaml:"refresh_cadence"` + StaleAfter string `yaml:"stale_after"` + Note string `yaml:"note"` + Source struct { + Type string `yaml:"type"` + URL string `yaml:"url"` + ArtifactIdentity string `yaml:"artifact_identity"` + Confidence string `yaml:"confidence"` + CapturedAt string `yaml:"captured_at"` + } `yaml:"source"` } `yaml:"entries"` } @@ -37,7 +50,7 @@ var doctorCmd = &cobra.Command{ } func init() { - doctorCmd.Flags().StringVar(&doctorCatalog, "catalog", defaultCatalogPath, "Provider catalog file path") + doctorCmd.Flags().StringVar(&doctorCatalog, "catalog", "", "Provider catalog file path (required)") } func runDoctor(cmd *cobra.Command, args []string) error { @@ -64,6 +77,8 @@ func runDoctor(cmd *cobra.Command, args []string) error { executable, executableErr := os.Executable() if executableErr != nil { report("fail", "binary", executableDetail(executable, executableErr)) + } else if !supportedExecutableName(executable, runtime.GOOS) { + report("warn", "binary", fmt.Sprintf("%s has unexpected executable name; supported name is profitctl", executable)) } else { report("ok", "binary", executable) } @@ -95,16 +110,20 @@ func runDoctor(cmd *cobra.Command, args []string) error { report("ok", "config", cfgFile) } - catalog, catalogErr := readCatalog(doctorCatalog) - if catalogErr != nil { - report("fail", "catalog", fmt.Sprintf("%s: %v; pass a readable catalog with `--catalog PATH`", doctorCatalog, catalogErr)) + if strings.TrimSpace(doctorCatalog) == "" { + report("fail", "catalog", "catalog path is required; pass a provenance-complete catalog with `--catalog PATH`") } else { - fresh, freshness := catalogFreshness(catalog.CatalogVersion, time.Now()) - detail := fmt.Sprintf("%s (%s, %d entries; %s)", doctorCatalog, catalog.CatalogVersion, len(catalog.Entries), freshness) - if fresh { - report("ok", "catalog", detail) + catalog, catalogErr := readCatalog(doctorCatalog) + if catalogErr != nil { + report("fail", "catalog", fmt.Sprintf("%s: %v; pass a provenance-complete catalog with `--catalog PATH`", doctorCatalog, catalogErr)) } else { - report("warn", "catalog", detail+"; stale catalog cannot support current-price claims") + fresh, freshness := catalogFreshness(catalog, time.Now()) + detail := fmt.Sprintf("%s (%s, %d entries; %s)", doctorCatalog, catalog.CatalogVersion, len(catalog.Entries), freshness) + if fresh { + report("ok", "catalog", detail) + } else { + report("warn", "catalog", detail+"; stale catalog cannot support current-price claims") + } } } @@ -137,19 +156,29 @@ func supportedRuntime(goos, goarch string) bool { } } -func catalogFreshness(catalogVersion string, now time.Time) (bool, string) { - capturedAt, err := time.Parse("2006-01-02", catalogVersion) - if err != nil { - return false, "capture date is not YYYY-MM-DD" +func supportedExecutableName(path, goos string) bool { + name := filepath.Base(strings.ReplaceAll(path, `\`, "/")) + if goos == "windows" { + return strings.EqualFold(name, "profitctl.exe") } - ageDays := int(now.UTC().Sub(capturedAt).Hours() / 24) - if ageDays < 0 { - return false, "capture date is in the future" + return name == "profitctl" +} + +func catalogFreshness(catalog *catalogProbe, now time.Time) (bool, string) { + staleEntries := 0 + for _, entry := range catalog.Entries { + staleAfter, err := time.Parse("2006-01-02", entry.StaleAfter) + if err != nil { + return false, fmt.Sprintf("entry %s stale_after is not YYYY-MM-DD", entry.ID) + } + if now.UTC().After(staleAfter.Add(24*time.Hour - time.Nanosecond)) { + staleEntries++ + } } - if ageDays > 30 { - return false, fmt.Sprintf("%d days old; 30-day freshness window exceeded", ageDays) + if staleEntries > 0 { + return false, fmt.Sprintf("%d of %d entries are stale", staleEntries, len(catalog.Entries)) } - return true, fmt.Sprintf("%d days old", ageDays) + return true, fmt.Sprintf("%d entries within declared stale-after dates", len(catalog.Entries)) } func readCatalog(path string) (*catalogProbe, error) { @@ -176,6 +205,29 @@ func readCatalog(path string) (*catalogProbe, error) { strings.TrimSpace(entry.Service) == "" || strings.TrimSpace(entry.Unit) == "" { return nil, fmt.Errorf("entry %d requires id, provider, service, and unit", i+1) } + if entry.Value == nil || strings.TrimSpace(entry.Currency) == "" { + return nil, fmt.Errorf("entry %s requires value and currency", entry.ID) + } + if strings.TrimSpace(entry.Source.Type) == "" || strings.TrimSpace(entry.Source.Confidence) == "" || + strings.TrimSpace(entry.Source.CapturedAt) == "" { + return nil, fmt.Errorf("entry %s requires source type, confidence, and captured_at", entry.ID) + } + if strings.TrimSpace(entry.Source.URL) == "" && strings.TrimSpace(entry.Source.ArtifactIdentity) == "" { + return nil, fmt.Errorf("entry %s requires source url or artifact_identity", entry.ID) + } + if _, err := time.Parse("2006-01-02", entry.Source.CapturedAt); err != nil { + return nil, fmt.Errorf("entry %s source captured_at must be YYYY-MM-DD", entry.ID) + } + if strings.TrimSpace(entry.ConfidenceRationale) == "" || strings.TrimSpace(entry.Note) == "" { + return nil, fmt.Errorf("entry %s requires confidence_rationale and note", entry.ID) + } + if strings.TrimSpace(entry.RefreshOwner) == "" || strings.TrimSpace(entry.RefreshCadence) == "" || + strings.TrimSpace(entry.StaleAfter) == "" { + return nil, fmt.Errorf("entry %s requires refresh_owner, refresh_cadence, and stale_after", entry.ID) + } + if _, err := time.Parse("2006-01-02", entry.StaleAfter); err != nil { + return nil, fmt.Errorf("entry %s stale_after must be YYYY-MM-DD", entry.ID) + } } return &catalog, nil } diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index 5433fd6..58b5ac2 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -16,7 +16,7 @@ import ( func TestRunDoctorPassesWithValidLocalInputs(t *testing.T) { oldCatalog := doctorCatalog defer func() { doctorCatalog = oldCatalog }() - doctorCatalog = filepath.Join("..", "provider_catalog", "ai_saas_defaults.yml") + doctorCatalog = filepath.Join("..", "test", "fixtures", "provider_catalog_valid.yml") command := doctorTestCommand(filepath.Join("..", "examples", "valid_profit.yml")) var output bytes.Buffer @@ -24,10 +24,9 @@ func TestRunDoctorPassesWithValidLocalInputs(t *testing.T) { err := runDoctor(command, nil) require.NoError(t, err) - assert.Contains(t, output.String(), "[ok] binary:") + assert.Contains(t, output.String(), "[warn] binary:") assert.Contains(t, output.String(), "[ok] config:") - assert.Contains(t, output.String(), "[warn] catalog:") - assert.Contains(t, output.String(), "stale catalog cannot support current-price claims") + assert.Contains(t, output.String(), "[ok] catalog:") assert.Contains(t, output.String(), "doctor passed") } @@ -63,16 +62,44 @@ func TestReadCatalogRejectsIncompleteEntry(t *testing.T) { assert.Contains(t, err.Error(), "requires id, provider, service, and unit") } +func TestReadCatalogRequiresProvenanceFields(t *testing.T) { + path := filepath.Join(t.TempDir(), "catalog.yml") + err := os.WriteFile(path, []byte(` +catalog_version: test-v1 +status: test +entries: + - id: missing-provenance + provider: synthetic + service: test + unit: request +`), 0644) + require.NoError(t, err) + + _, err = readCatalog(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "requires value and currency") +} + func TestCatalogFreshness(t *testing.T) { now := time.Date(2026, time.July, 31, 0, 0, 0, 0, time.UTC) + catalog, err := readCatalog(filepath.Join("..", "test", "fixtures", "provider_catalog_valid.yml")) + require.NoError(t, err) - fresh, detail := catalogFreshness("2026-07-15", now) + fresh, detail := catalogFreshness(catalog, now) assert.True(t, fresh) - assert.Contains(t, detail, "16 days old") + assert.Contains(t, detail, "within declared stale-after dates") - fresh, detail = catalogFreshness("2026-05-29", now) + catalog.Entries[0].StaleAfter = "2026-07-01" + fresh, detail = catalogFreshness(catalog, now) assert.False(t, fresh) - assert.Contains(t, detail, "30-day freshness window exceeded") + assert.Contains(t, detail, "1 of 1 entries are stale") +} + +func TestSupportedExecutableName(t *testing.T) { + assert.True(t, supportedExecutableName("/tmp/profitctl", "linux")) + assert.True(t, supportedExecutableName(`C:\bin\profitctl.exe`, "windows")) + assert.False(t, supportedExecutableName("/tmp/ProfitCtl", "linux")) + assert.False(t, supportedExecutableName("/tmp/profitctl-old", "linux")) } func doctorTestCommand(configPath string) *cobra.Command { diff --git a/test/fixtures/provider_catalog_valid.yml b/test/fixtures/provider_catalog_valid.yml new file mode 100644 index 0000000..89c8a38 --- /dev/null +++ b/test/fixtures/provider_catalog_valid.yml @@ -0,0 +1,19 @@ +catalog_version: test-v1 +status: synthetic_test_fixture +entries: + - id: synthetic_request_cost + provider: synthetic + service: request_api + unit: request + value: 0.001 + currency: USD + source: + type: user_supplied + artifact_identity: test/fixtures/provider_catalog_valid.yml + confidence: medium + captured_at: 2026-07-31 + confidence_rationale: Deterministic local fixture for CLI validation only. + refresh_owner: test-fixture-maintainer + refresh_cadence: on_fixture_change + stale_after: 2099-01-01 + note: Synthetic amount; excludes taxes, discounts, minimums, and live provider data. From 32814aa131e203feb2158d94287ad1a24801ad3a Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Fri, 31 Jul 2026 17:40:29 -0700 Subject: [PATCH 3/6] fix(cli): reject invalid catalog readiness --- README.md | 1 - cmd/doctor.go | 42 +++++++++++++++++++++++++++++++++++++++++- cmd/doctor_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ docs/INSTALL.md | 10 +++++++++- 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 11a9d5d..51f4aaf 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ It helps teams model fixed + variable costs, simulate growth and stress scenario ```bash curl -fsSL https://raw.githubusercontent.com/IntelIP/ProfitCtl/main/scripts/install.sh | bash -profitctl --version ``` The installer downloads a GitHub Release archive, verifies it against that release's `SHA256SUMS`, and installs `profitctl` to `~/.local/bin` unless a custom prefix is selected. diff --git a/cmd/doctor.go b/cmd/doctor.go index 404a429..d7e081f 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -3,6 +3,7 @@ package cmd import ( "errors" "fmt" + "math" "os" "path/filepath" "runtime" @@ -10,6 +11,7 @@ import ( "time" "github.com/IntelIP/ProfitCtl/internal/config" + "github.com/IntelIP/ProfitCtl/pkg/types" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -208,16 +210,29 @@ func readCatalog(path string) (*catalogProbe, error) { if entry.Value == nil || strings.TrimSpace(entry.Currency) == "" { return nil, fmt.Errorf("entry %s requires value and currency", entry.ID) } + if *entry.Value < 0 || math.IsNaN(*entry.Value) || math.IsInf(*entry.Value, 0) { + return nil, fmt.Errorf("entry %s value must be finite and non-negative", entry.ID) + } if strings.TrimSpace(entry.Source.Type) == "" || strings.TrimSpace(entry.Source.Confidence) == "" || strings.TrimSpace(entry.Source.CapturedAt) == "" { return nil, fmt.Errorf("entry %s requires source type, confidence, and captured_at", entry.ID) } + if !supportedSourceType(entry.Source.Type) { + return nil, fmt.Errorf("entry %s has unsupported source type %q", entry.ID, entry.Source.Type) + } + if !supportedSourceConfidence(entry.Source.Confidence) { + return nil, fmt.Errorf("entry %s has unsupported source confidence %q", entry.ID, entry.Source.Confidence) + } if strings.TrimSpace(entry.Source.URL) == "" && strings.TrimSpace(entry.Source.ArtifactIdentity) == "" { return nil, fmt.Errorf("entry %s requires source url or artifact_identity", entry.ID) } - if _, err := time.Parse("2006-01-02", entry.Source.CapturedAt); err != nil { + capturedAt, err := time.Parse("2006-01-02", entry.Source.CapturedAt) + if err != nil { return nil, fmt.Errorf("entry %s source captured_at must be YYYY-MM-DD", entry.ID) } + if capturedAt.After(time.Now().UTC()) { + return nil, fmt.Errorf("entry %s source captured_at cannot be in the future", entry.ID) + } if strings.TrimSpace(entry.ConfidenceRationale) == "" || strings.TrimSpace(entry.Note) == "" { return nil, fmt.Errorf("entry %s requires confidence_rationale and note", entry.ID) } @@ -231,3 +246,28 @@ func readCatalog(path string) (*catalogProbe, error) { } return &catalog, nil } + +func supportedSourceType(value string) bool { + switch types.CostSourceType(value) { + case types.CostSourceTemplate, + types.CostSourceUserSupplied, + types.CostSourceRepoDetected, + types.CostSourceTelemetry, + types.CostSourceInvoice, + types.CostSourceProviderCatalog: + return true + default: + return false + } +} + +func supportedSourceConfidence(value string) bool { + switch types.CostSourceConfidence(value) { + case types.CostSourceConfidenceLow, + types.CostSourceConfidenceMedium, + types.CostSourceConfidenceHigh: + return true + default: + return false + } +} diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index 58b5ac2..efc612b 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -5,6 +5,7 @@ import ( "errors" "os" "path/filepath" + "strings" "testing" "time" @@ -80,6 +81,38 @@ entries: assert.Contains(t, err.Error(), "requires value and currency") } +func TestReadCatalogRejectsInvalidValue(t *testing.T) { + for _, value := range []string{"-0.001", ".nan", ".inf"} { + t.Run(value, func(t *testing.T) { + path := writeCatalogVariant(t, "value: 0.001", "value: "+value) + + _, err := readCatalog(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "value must be finite and non-negative") + }) + } +} + +func TestReadCatalogRejectsInvalidProvenanceEnums(t *testing.T) { + sourcePath := writeCatalogVariant(t, "type: user_supplied", "type: provider-catalg") + _, err := readCatalog(sourcePath) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported source type") + + confidencePath := writeCatalogVariant(t, "confidence: medium", "confidence: medum") + _, err = readCatalog(confidencePath) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported source confidence") +} + +func TestReadCatalogRejectsFutureCaptureDate(t *testing.T) { + path := writeCatalogVariant(t, "captured_at: 2026-07-31", "captured_at: 2099-01-01") + + _, err := readCatalog(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "captured_at cannot be in the future") +} + func TestCatalogFreshness(t *testing.T) { now := time.Date(2026, time.July, 31, 0, 0, 0, 0, time.UTC) catalog, err := readCatalog(filepath.Join("..", "test", "fixtures", "provider_catalog_valid.yml")) @@ -107,3 +140,15 @@ func doctorTestCommand(configPath string) *cobra.Command { command.Flags().String("file", configPath, "") return command } + +func writeCatalogVariant(t *testing.T, oldValue, newValue string) string { + t.Helper() + data, err := os.ReadFile(filepath.Join("..", "test", "fixtures", "provider_catalog_valid.yml")) + require.NoError(t, err) + require.Contains(t, string(data), oldValue) + + path := filepath.Join(t.TempDir(), "catalog.yml") + err = os.WriteFile(path, []byte(strings.Replace(string(data), oldValue, newValue, 1)), 0644) + require.NoError(t, err) + return path +} diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 3d1d497..2397f31 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -65,7 +65,15 @@ go install github.com/IntelIP/ProfitCtl/cmd/profitctl@ Source and Go installs are explicit developer fallbacks. The checksum-verified GitHub Release installer is the primary supported path; Homebrew is secondary. -## Verify +## Verify the Current Published Install + +```bash +profitctl --help +``` + +## Verify a Release Candidate + +Use these checks for an artifact built from a commit containing the stable CLI identity contract. Publishing that artifact and updating Homebrew remain separate release actions. ```bash profitctl --version From ca6f9f6ea2f2e6cabb8a1b2502a4cf4e3fbb711d Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Fri, 31 Jul 2026 18:08:17 -0700 Subject: [PATCH 4/6] fix(cli): enforce catalog freshness policy --- cmd/doctor.go | 39 +++++++++++++++++++++++++++++++-------- cmd/doctor_test.go | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/cmd/doctor.go b/cmd/doctor.go index d7e081f..a186c06 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -124,7 +124,7 @@ func runDoctor(cmd *cobra.Command, args []string) error { if fresh { report("ok", "catalog", detail) } else { - report("warn", "catalog", detail+"; stale catalog cannot support current-price claims") + report("fail", "catalog", detail+"; stale catalog cannot support current-price or release-readiness claims") } } } @@ -167,18 +167,30 @@ func supportedExecutableName(path, goos string) bool { } func catalogFreshness(catalog *catalogProbe, now time.Time) (bool, string) { - staleEntries := 0 for _, entry := range catalog.Entries { staleAfter, err := time.Parse("2006-01-02", entry.StaleAfter) if err != nil { return false, fmt.Sprintf("entry %s stale_after is not YYYY-MM-DD", entry.ID) } if now.UTC().After(staleAfter.Add(24*time.Hour - time.Nanosecond)) { - staleEntries++ + return false, fmt.Sprintf("entry %s exceeded stale_after %s", entry.ID, entry.StaleAfter) + } + + capturedAt, err := parseCapturedAt(entry.Source.CapturedAt) + if err != nil { + return false, fmt.Sprintf("entry %s source captured_at is invalid", entry.ID) + } + age := now.UTC().Sub(capturedAt) + switch types.CostSourceType(entry.Source.Type) { + case types.CostSourceProviderCatalog: + if age > 7*24*time.Hour { + return false, fmt.Sprintf("entry %s provider catalog capture exceeds the 7-day decision-grade window", entry.ID) + } + case types.CostSourceTemplate: + if age > 90*24*time.Hour { + return false, fmt.Sprintf("entry %s template capture exceeds the 90-day review window", entry.ID) + } } - } - if staleEntries > 0 { - return false, fmt.Sprintf("%d of %d entries are stale", staleEntries, len(catalog.Entries)) } return true, fmt.Sprintf("%d entries within declared stale-after dates", len(catalog.Entries)) } @@ -226,9 +238,9 @@ func readCatalog(path string) (*catalogProbe, error) { if strings.TrimSpace(entry.Source.URL) == "" && strings.TrimSpace(entry.Source.ArtifactIdentity) == "" { return nil, fmt.Errorf("entry %s requires source url or artifact_identity", entry.ID) } - capturedAt, err := time.Parse("2006-01-02", entry.Source.CapturedAt) + capturedAt, err := parseCapturedAt(entry.Source.CapturedAt) if err != nil { - return nil, fmt.Errorf("entry %s source captured_at must be YYYY-MM-DD", entry.ID) + return nil, fmt.Errorf("entry %s source captured_at must be YYYY-MM-DD or RFC3339", entry.ID) } if capturedAt.After(time.Now().UTC()) { return nil, fmt.Errorf("entry %s source captured_at cannot be in the future", entry.ID) @@ -247,6 +259,17 @@ func readCatalog(path string) (*catalogProbe, error) { return &catalog, nil } +func parseCapturedAt(value string) (time.Time, error) { + if parsed, err := time.Parse(time.RFC3339, value); err == nil { + return parsed.UTC(), nil + } + parsed, err := time.Parse("2006-01-02", value) + if err != nil { + return time.Time{}, err + } + return parsed.UTC(), nil +} + func supportedSourceType(value string) bool { switch types.CostSourceType(value) { case types.CostSourceTemplate, diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index efc612b..dfd8056 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -53,6 +53,22 @@ func TestRunDoctorReportsMissingInputsWithoutFallback(t *testing.T) { assert.Contains(t, output.String(), "no fallback or local state change was performed") } +func TestRunDoctorFailsStaleCatalog(t *testing.T) { + oldCatalog := doctorCatalog + defer func() { doctorCatalog = oldCatalog }() + doctorCatalog = writeCatalogVariant(t, "stale_after: 2099-01-01", "stale_after: 2026-07-01") + + command := doctorTestCommand(filepath.Join("..", "examples", "valid_profit.yml")) + var output bytes.Buffer + command.SetOut(&output) + + err := runDoctor(command, nil) + require.Error(t, err) + assert.Equal(t, 2, ExitCode(err)) + assert.Contains(t, output.String(), "[fail] catalog:") + assert.Contains(t, output.String(), "stale catalog cannot support current-price or release-readiness claims") +} + func TestReadCatalogRejectsIncompleteEntry(t *testing.T) { path := filepath.Join(t.TempDir(), "catalog.yml") err := os.WriteFile(path, []byte("catalog_version: today\nstatus: test\nentries:\n - id: incomplete\n"), 0644) @@ -113,6 +129,13 @@ func TestReadCatalogRejectsFutureCaptureDate(t *testing.T) { assert.Contains(t, err.Error(), "captured_at cannot be in the future") } +func TestReadCatalogAcceptsRFC3339CaptureTimestamp(t *testing.T) { + path := writeCatalogVariant(t, "captured_at: 2026-07-31", "captured_at: 2026-07-31T12:00:00Z") + + _, err := readCatalog(path) + require.NoError(t, err) +} + func TestCatalogFreshness(t *testing.T) { now := time.Date(2026, time.July, 31, 0, 0, 0, 0, time.UTC) catalog, err := readCatalog(filepath.Join("..", "test", "fixtures", "provider_catalog_valid.yml")) @@ -125,7 +148,22 @@ func TestCatalogFreshness(t *testing.T) { catalog.Entries[0].StaleAfter = "2026-07-01" fresh, detail = catalogFreshness(catalog, now) assert.False(t, fresh) - assert.Contains(t, detail, "1 of 1 entries are stale") + assert.Contains(t, detail, "exceeded stale_after") +} + +func TestCatalogFreshnessBoundsProviderCaptureAge(t *testing.T) { + now := time.Date(2026, time.July, 31, 0, 0, 0, 0, time.UTC) + path := writeCatalogVariant(t, "type: user_supplied", "type: provider_catalog") + data, err := os.ReadFile(path) + require.NoError(t, err) + err = os.WriteFile(path, []byte(strings.Replace(string(data), "captured_at: 2026-07-31", "captured_at: 2026-07-01", 1)), 0644) + require.NoError(t, err) + catalog, err := readCatalog(path) + require.NoError(t, err) + + fresh, detail := catalogFreshness(catalog, now) + assert.False(t, fresh) + assert.Contains(t, detail, "7-day decision-grade window") } func TestSupportedExecutableName(t *testing.T) { From 918498b931ad0b292f27991cfe1fc65cf21a9820 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Fri, 31 Jul 2026 18:20:32 -0700 Subject: [PATCH 5/6] fix: align doctor provenance checks --- cmd/doctor.go | 18 ++++++++++++++++++ cmd/doctor_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/cmd/doctor.go b/cmd/doctor.go index a186c06..5f43a18 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "math" + "net/url" "os" "path/filepath" "runtime" @@ -235,9 +236,15 @@ func readCatalog(path string) (*catalogProbe, error) { if !supportedSourceConfidence(entry.Source.Confidence) { return nil, fmt.Errorf("entry %s has unsupported source confidence %q", entry.ID, entry.Source.Confidence) } + if entry.Source.Type == string(types.CostSourceTemplate) && entry.Source.Confidence == "high" { + return nil, fmt.Errorf("entry %s template source cannot use high confidence", entry.ID) + } if strings.TrimSpace(entry.Source.URL) == "" && strings.TrimSpace(entry.Source.ArtifactIdentity) == "" { return nil, fmt.Errorf("entry %s requires source url or artifact_identity", entry.ID) } + if strings.TrimSpace(entry.Source.URL) != "" && !validSourceURI(entry.Source.URL) { + return nil, fmt.Errorf("entry %s source url must be an absolute URI", entry.ID) + } capturedAt, err := parseCapturedAt(entry.Source.CapturedAt) if err != nil { return nil, fmt.Errorf("entry %s source captured_at must be YYYY-MM-DD or RFC3339", entry.ID) @@ -259,6 +266,17 @@ func readCatalog(path string) (*catalogProbe, error) { return &catalog, nil } +func validSourceURI(value string) bool { + parsed, err := url.ParseRequestURI(strings.TrimSpace(value)) + if err != nil || !parsed.IsAbs() { + return false + } + if (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host == "" { + return false + } + return true +} + func parseCapturedAt(value string) (time.Time, error) { if parsed, err := time.Parse(time.RFC3339, value); err == nil { return parsed.UTC(), nil diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index dfd8056..87db1b4 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -121,6 +121,30 @@ func TestReadCatalogRejectsInvalidProvenanceEnums(t *testing.T) { assert.Contains(t, err.Error(), "unsupported source confidence") } +func TestReadCatalogRejectsHighConfidenceTemplateSource(t *testing.T) { + path := writeCatalogVariant(t, "type: user_supplied", "type: template") + data, err := os.ReadFile(path) + require.NoError(t, err) + err = os.WriteFile(path, []byte(strings.Replace(string(data), "confidence: medium", "confidence: high", 1)), 0644) + require.NoError(t, err) + + _, err = readCatalog(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "template source cannot use high confidence") +} + +func TestReadCatalogRejectsMalformedSourceURL(t *testing.T) { + path := writeCatalogVariant( + t, + "artifact_identity: test/fixtures/provider_catalog_valid.yml", + "url: https//example.invalid/pricing", + ) + + _, err := readCatalog(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "source url must be an absolute URI") +} + func TestReadCatalogRejectsFutureCaptureDate(t *testing.T) { path := writeCatalogVariant(t, "captured_at: 2026-07-31", "captured_at: 2099-01-01") From c2f2518c5ae1e2c68c6aded30e5804fd1827dd2c Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Fri, 31 Jul 2026 18:36:34 -0700 Subject: [PATCH 6/6] fix: harden release readiness checks --- README.md | 4 +++- cmd/doctor.go | 4 +++- cmd/doctor_test.go | 19 +++++++++++++++++++ docs/INSTALL.md | 5 ++++- .../PCTL-3-PCTL-14-decision-packet.md | 1 + scripts/release/smoke-published-release.sh | 5 +++++ 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 51f4aaf..0469a88 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,9 @@ go build -o profitctl . go install github.com/IntelIP/ProfitCtl/cmd/profitctl@ ``` -Go-based paths are explicit developer fallbacks. GitHub Release installation is the supported user path; Homebrew is secondary. +Choose a commit or future tag that contains `cmd/profitctl`; the current published +`v0.2.0` tag predates this package. Go-based paths are explicit developer fallbacks. +GitHub Release installation is the supported user path; Homebrew is secondary. ## Quick Usage diff --git a/cmd/doctor.go b/cmd/doctor.go index 5f43a18..1cee006 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -106,9 +106,11 @@ func runDoctor(cmd *cobra.Command, args []string) error { report("ok", "runtime", runtimeDetail) } - _, configErr := config.ParseConfig(cfgFile) + parsedConfig, configErr := config.ParseConfig(cfgFile) if configErr != nil { report("fail", "config", fmt.Sprintf("%s: %v; create or select one with `profitctl init --file %s`", cfgFile, configErr, cfgFile)) + } else if parsedConfig.Simulation == nil { + report("fail", "config", fmt.Sprintf("%s: simulation configuration is required", cfgFile)) } else { report("ok", "config", cfgFile) } diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index 87db1b4..df6849f 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -53,6 +53,25 @@ func TestRunDoctorReportsMissingInputsWithoutFallback(t *testing.T) { assert.Contains(t, output.String(), "no fallback or local state change was performed") } +func TestRunDoctorRejectsConfigWithoutSimulation(t *testing.T) { + oldCatalog := doctorCatalog + defer func() { doctorCatalog = oldCatalog }() + doctorCatalog = filepath.Join("..", "test", "fixtures", "provider_catalog_valid.yml") + + configPath := filepath.Join(t.TempDir(), "profit.yml") + require.NoError(t, os.WriteFile(configPath, []byte("{}\n"), 0644)) + + command := doctorTestCommand(configPath) + var output bytes.Buffer + command.SetOut(&output) + + err := runDoctor(command, nil) + require.Error(t, err) + assert.Equal(t, 2, ExitCode(err)) + assert.Contains(t, output.String(), "[fail] config:") + assert.Contains(t, output.String(), "simulation configuration is required") +} + func TestRunDoctorFailsStaleCatalog(t *testing.T) { oldCatalog := doctorCatalog defer func() { doctorCatalog = oldCatalog }() diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 2397f31..ac0a5b9 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -63,7 +63,10 @@ go build -o profitctl . go install github.com/IntelIP/ProfitCtl/cmd/profitctl@ ``` -Source and Go installs are explicit developer fallbacks. The checksum-verified GitHub Release installer is the primary supported path; Homebrew is secondary. +Choose a commit or future tag that contains `cmd/profitctl`; the current published +`v0.2.0` tag predates this package. Source and Go installs are explicit developer +fallbacks. The checksum-verified GitHub Release installer is the primary supported +path; Homebrew is secondary. ## Verify the Current Published Install diff --git a/docs/decisions/PCTL-3-PCTL-14-decision-packet.md b/docs/decisions/PCTL-3-PCTL-14-decision-packet.md index 4e1438e..3b63300 100644 --- a/docs/decisions/PCTL-3-PCTL-14-decision-packet.md +++ b/docs/decisions/PCTL-3-PCTL-14-decision-packet.md @@ -158,6 +158,7 @@ PCTL-20 should add a source-backed Upstash catalog entry only after policy accep - Primary install: checksum-verified GitHub Release install script into a user-selected or temporary prefix. - Secondary install: Homebrew formula pinned to the same release. - Development fallback: explicit `go run .` or `go install github.com/IntelIP/ProfitCtl/cmd/profitctl@`; never silent. The command package path is required because installing the mixed-case module root would create a `ProfitCtl` binary instead of the supported `profitctl` identity. +- Go-install availability: select a commit or future tag containing `cmd/profitctl`; the current published `v0.2.0` tag predates that package. - Version behavior: `profitctl version` and `profitctl --version` report injected package tag; development builds report an explicit development identity. - Doctor behavior: inspect current executable identity, configuration, provider catalog presence/age, and command prerequisites; explain failures; exit nonzero on missing required dependency; never switch binaries or mutate machine-global installation. - Release number, publication, merge, and deployment remain separate decisions. diff --git a/scripts/release/smoke-published-release.sh b/scripts/release/smoke-published-release.sh index 95707c8..957e691 100755 --- a/scripts/release/smoke-published-release.sh +++ b/scripts/release/smoke-published-release.sh @@ -83,7 +83,12 @@ BIN_PATH="$(find "${EXTRACT_DIR}" -type f -name profitctl -perm -111 | head -n 1 } "${BIN_PATH}" --help >/dev/null +test "$("${BIN_PATH}" --version)" = "profitctl ${TAG}" +test "$("${BIN_PATH}" version)" = "profitctl ${TAG}" "${BIN_PATH}" validate -f "${ROOT}/examples/mix_profit.yml" >/dev/null +"${BIN_PATH}" doctor \ + -f "${ROOT}/examples/mix_profit.yml" \ + --catalog "${ROOT}/test/fixtures/provider_catalog_valid.yml" >/dev/null "${BIN_PATH}" compare \ "${ROOT}/benchmark_scenarios/open_core_tiered.yml" \ "${ROOT}/benchmark_scenarios/open_core_mix.yml" >/dev/null