diff --git a/.gitignore b/.gitignore index 4a7fcee..0a20cab 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Go /vendor/ +/go/ go.work go.work.sum coverage.out diff --git a/cmd/optiqor/golden_test.go b/cmd/optiqor/golden_test.go index 3f93bc8..3c3e22e 100644 --- a/cmd/optiqor/golden_test.go +++ b/cmd/optiqor/golden_test.go @@ -68,10 +68,13 @@ func TestCmd_Golden_Stable(t *testing.T) { // output so goldens stay bit-identical across laptops and CI runners. func normalize(s string) string { if cwd, err := os.Getwd(); err == nil { + s = strings.ReplaceAll(s, strings.ReplaceAll(cwd, "\\", "\\\\"), "") s = strings.ReplaceAll(s, cwd, "") if repo, err := filepath.Abs(filepath.Join(cwd, "..", "..")); err == nil { + s = strings.ReplaceAll(s, strings.ReplaceAll(repo, "\\", "\\\\"), "") s = strings.ReplaceAll(s, repo, "") } } return s } + diff --git a/pkg/rules/excessive_replicas.go b/pkg/rules/excessive_replicas.go index 0f88349..4a12684 100644 --- a/pkg/rules/excessive_replicas.go +++ b/pkg/rules/excessive_replicas.go @@ -36,5 +36,14 @@ func (excessiveReplicaCount) Run(w parser.Workload) []Finding { Detail: fmt.Sprintf("Replicas set to %d. Past ~20 replicas the cost grows linearly while the marginal availability gain approaches zero — most cloud zones can't fit that many across enough fault domains. Cap the HPA's maxReplicas or split the workload across multiple deployments.", w.Replicas), Severity: SeverityMed, Confidence: ConfidenceMed, + Signal: &Signal{ + Label: "replicas", + Have: float64(excessiveReplicaThreshold), + Want: float64(w.Replicas), + HaveDisplay: "20", + WantDisplay: fmt.Sprintf("%d", w.Replicas), + Note: fmt.Sprintf("%dx HA threshold", w.Replicas/excessiveReplicaThreshold), + }, }} } + diff --git a/pkg/rules/incomplete_request.go b/pkg/rules/incomplete_request.go index b4528ee..e71aa24 100644 --- a/pkg/rules/incomplete_request.go +++ b/pkg/rules/incomplete_request.go @@ -33,6 +33,14 @@ func (cpuWithoutMemoryRequest) Run(w parser.Workload) []Finding { Detail: "requests.cpu is set but requests.memory is not. The scheduler can't reserve memory accurately, so the pod is BestEffort for memory — first to evict under pressure. Add a memory request matching observed P95.", Severity: SeverityLow, Confidence: ConfidenceHigh, + Signal: &Signal{ + Label: "memory", + Have: 0, + Want: 1, + HaveDisplay: "unset", + WantDisplay: "missing", + Note: "asymmetric request", + }, }} } @@ -54,5 +62,14 @@ func (memoryWithoutCPURequest) Run(w parser.Workload) []Finding { Detail: "requests.memory is set but requests.cpu is not. The scheduler can't reserve CPU, so bin-packing assumes zero — pods can stack onto a single node and starve each other. Add a CPU request matching observed P95.", Severity: SeverityLow, Confidence: ConfidenceHigh, + Signal: &Signal{ + Label: "CPU", + Have: 0, + Want: 1, + HaveDisplay: "unset", + WantDisplay: "missing", + Note: "asymmetric request", + }, }} } + diff --git a/pkg/rules/oversized_limits.go b/pkg/rules/oversized_limits.go index 2bdda32..ef636c5 100644 --- a/pkg/rules/oversized_limits.go +++ b/pkg/rules/oversized_limits.go @@ -38,6 +38,14 @@ func (oversizedCPULimit) Run(w parser.Workload) []Finding { Detail: fmt.Sprintf("CPU limit is %s. Above 4 vCPU, the pod can only land on large nodes; smaller / Spot instance types are excluded from bin-packing. Either split the workload or confirm the high limit is justified by P99.", w.Limits.CPU), Severity: SeverityMed, Confidence: ConfidenceMed, + Signal: &Signal{ + Label: "CPU", + Have: float64(oversizedCPULimitMillicores), + Want: float64(w.Limits.CPU.Value), + HaveDisplay: "4", + WantDisplay: w.Limits.CPU.String(), + Note: "exceeds large-node threshold", + }, }} } @@ -59,5 +67,14 @@ func (oversizedMemoryLimit) Run(w parser.Workload) []Finding { Detail: fmt.Sprintf("Memory limit is %s. Above 16 GiB, the pod can only land on memory-class nodes; balanced / Spot bin-packing is excluded. Confirm the workload genuinely uses this much, or split the workload.", w.Limits.Memory), Severity: SeverityMed, Confidence: ConfidenceMed, + Signal: &Signal{ + Label: "memory", + Have: float64(oversizedMemoryLimitBytes), + Want: float64(w.Limits.Memory.Value), + HaveDisplay: "16Gi", + WantDisplay: w.Limits.Memory.String(), + Note: "exceeds large-node threshold", + }, }} } + diff --git a/pkg/rules/tiny_request.go b/pkg/rules/tiny_request.go index 89b5060..7a37c05 100644 --- a/pkg/rules/tiny_request.go +++ b/pkg/rules/tiny_request.go @@ -38,6 +38,14 @@ func (tinyCPURequest) Run(w parser.Workload) []Finding { Detail: fmt.Sprintf("requests.cpu is %s — below the 10m threshold most charts use as a sentinel. Probably a placeholder from a Helm scaffold. Set it to your observed P95 (or remove the limit-without-request asymmetry the scheduler is currently dealing with).", w.Requests.CPU), Severity: SeverityLow, Confidence: ConfidenceHigh, + Signal: &Signal{ + Label: "CPU", + Have: float64(w.Requests.CPU.Value), + Want: float64(tinyCPUMillicores), + HaveDisplay: w.Requests.CPU.String(), + WantDisplay: "10m", + Note: "below placeholder floor", + }, }} } @@ -62,5 +70,14 @@ func (tinyMemoryRequest) Run(w parser.Workload) []Finding { Detail: fmt.Sprintf("requests.memory is %s — below 32 MiB. A workload that genuinely needs less memory is a rarity; most charts setting tiny memory requests are using a placeholder. Set it to your observed P95.", w.Requests.Memory), Severity: SeverityLow, Confidence: ConfidenceHigh, + Signal: &Signal{ + Label: "memory", + Have: float64(w.Requests.Memory.Value), + Want: float64(tinyMemoryBytes), + HaveDisplay: w.Requests.Memory.String(), + WantDisplay: "32Mi", + Note: "below placeholder floor", + }, }} } + diff --git a/testdata/golden/analyze_fixture_detector_filter.txt b/testdata/golden/analyze_fixture_detector_filter.txt index 23222d2..239f6bc 100644 --- a/testdata/golden/analyze_fixture_detector_filter.txt +++ b/testdata/golden/analyze_fixture_detector_filter.txt @@ -3,7 +3,7 @@ Helm chart cost optimization · security as a bonus ──────────────────────────────────────────────────────────────────────────────── - Source /testdata/fixtures/basic-chart/values.yaml + Source \testdata\fixtures\basic-chart\values.yaml Workloads 15 workloads analyzed Cost ✓ no cost waste detected Security 2 findings — bonus, surfaced while parsing diff --git a/testdata/golden/analyze_fixture_plain.txt b/testdata/golden/analyze_fixture_plain.txt index 15c9d01..d5cae1c 100644 --- a/testdata/golden/analyze_fixture_plain.txt +++ b/testdata/golden/analyze_fixture_plain.txt @@ -3,7 +3,7 @@ Helm chart cost optimization · security as a bonus ──────────────────────────────────────────────────────────────────────────────── - Source /testdata/fixtures/basic-chart/values.yaml + Source \testdata\fixtures\basic-chart\values.yaml Workloads 15 workloads analyzed Cost 25 optimizations · save ~$35.39/mo (~$424.68/yr) ±40% Security 49 findings — bonus, surfaced while parsing @@ -156,6 +156,8 @@ │ │ │ Replica count past the HA inflection point │ │ │ + │ replicas 20 ███████████████████░░░░░ 25 1x HA threshold │ + │ │ │ Replicas set to 25. Past ~20 replicas the cost grows linearly while the │ │ marginal availability gain approaches zero — most cloud zones can't fit │ │ that many across enough fault domains. Cap the HPA's maxReplicas or split │ @@ -168,6 +170,8 @@ │ │ │ CPU limit forces large-node scheduling │ │ │ + │ CPU 4 ████████████░░░░░░░░░░░░ 8 exceeds large-node threshold │ + │ │ │ CPU limit is 8. Above 4 vCPU, the pod can only land on large nodes; │ │ smaller / Spot instance types are excluded from bin-packing. Either split │ │ the workload or confirm the high limit is justified by P99. │ @@ -179,6 +183,8 @@ │ │ │ Memory limit forces large-node scheduling │ │ │ + │ memory 16Gi ████████████░░░░░░░░░░░░ 32Gi exceeds large-node threshold │ + │ │ │ Memory limit is 32Gi. Above 16 GiB, the pod can only land on memory-class │ │ nodes; balanced / Spot bin-packing is excluded. Confirm the workload │ │ genuinely uses this much, or split the workload. │ @@ -286,6 +292,8 @@ │ │ │ CPU request set without memory request │ │ │ + │ memory unset ░░░░░░░░░░░░░░░░░░░░░░░░ missing asymmetric request │ + │ │ │ requests.cpu is set but requests.memory is not. The scheduler can't │ │ reserve memory accurately, so the pod is BestEffort for memory — first │ │ to evict under pressure. Add a memory request matching observed P95. │ @@ -297,6 +305,8 @@ │ │ │ CPU request below the placeholder threshold │ │ │ + │ CPU 5m ████████████░░░░░░░░░░░░ 10m below placeholder floor │ + │ │ │ requests.cpu is 5m — below the 10m threshold most charts use as a │ │ sentinel. Probably a placeholder from a Helm scaffold. Set it to your │ │ observed P95 (or remove the limit-without-request asymmetry the scheduler │ @@ -309,6 +319,8 @@ │ │ │ Memory request set without CPU request │ │ │ + │ CPU unset ░░░░░░░░░░░░░░░░░░░░░░░░ missing asymmetric request │ + │ │ │ requests.memory is set but requests.cpu is not. The scheduler can't │ │ reserve CPU, so bin-packing assumes zero — pods can stack onto a single │ │ node and starve each other. Add a CPU request matching observed P95. │ @@ -320,6 +332,8 @@ │ │ │ Memory request below the placeholder threshold │ │ │ + │ memory 16Mi ████████████░░░░░░░░░░░░ 32Mi below placeholder floor │ + │ │ │ requests.memory is 16Mi — below 32 MiB. A workload that genuinely needs │ │ less memory is a rarity; most charts setting tiny memory requests are │ │ using a placeholder. Set it to your observed P95. │ diff --git a/testdata/golden/analyze_fixture_severity_high.txt b/testdata/golden/analyze_fixture_severity_high.txt index 8e0a880..d9a060d 100644 --- a/testdata/golden/analyze_fixture_severity_high.txt +++ b/testdata/golden/analyze_fixture_severity_high.txt @@ -3,7 +3,7 @@ Helm chart cost optimization · security as a bonus ──────────────────────────────────────────────────────────────────────────────── - Source /testdata/fixtures/basic-chart/values.yaml + Source \testdata\fixtures\basic-chart\values.yaml Workloads 15 workloads analyzed Cost ✓ no cost waste detected Security 12 findings — bonus, surfaced while parsing diff --git a/testdata/golden/audit_fixture_plain.txt b/testdata/golden/audit_fixture_plain.txt index c8e9f1c..6f919b8 100644 --- a/testdata/golden/audit_fixture_plain.txt +++ b/testdata/golden/audit_fixture_plain.txt @@ -3,7 +3,7 @@ Helm chart cost optimization · security as a bonus ──────────────────────────────────────────────────────────────────────────────── - Source /testdata/fixtures/basic-chart/values.yaml + Source \testdata\fixtures\basic-chart\values.yaml Workloads 15 workloads analyzed Cost ✓ no cost waste detected Security 49 findings — bonus, surfaced while parsing diff --git a/testdata/golden/demo_json.txt b/testdata/golden/demo_json.txt index 09191db..8039872 100644 --- a/testdata/golden/demo_json.txt +++ b/testdata/golden/demo_json.txt @@ -470,7 +470,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 0, + "want": 1, + "have_display": "unset", + "want_display": "missing", + "note": "asymmetric request" + } }, { "DetectorID": "tiny-cpu-request", @@ -481,7 +488,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 5, + "want": 10, + "have_display": "5m", + "want_display": "10m", + "note": "below placeholder floor" + } }, { "DetectorID": "allow-privilege-escalation", @@ -565,7 +579,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "replicas", + "have": 20, + "want": 25, + "have_display": "20", + "want_display": "25", + "note": "1x HA threshold" + } }, { "DetectorID": "oversized-cpu-limit", @@ -576,7 +597,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 4000, + "want": 8000, + "have_display": "4", + "want_display": "8", + "note": "exceeds large-node threshold" + } }, { "DetectorID": "oversized-memory-limit", @@ -587,7 +615,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 17179869184, + "want": 34359738368, + "have_display": "16Gi", + "want_display": "32Gi", + "note": "exceeds large-node threshold" + } }, { "DetectorID": "replicas-too-high", @@ -638,7 +673,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 0, + "want": 1, + "have_display": "unset", + "want_display": "missing", + "note": "asymmetric request" + } }, { "DetectorID": "tiny-memory-request", @@ -649,7 +691,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 16777216, + "want": 33554432, + "have_display": "16Mi", + "want_display": "32Mi", + "note": "below placeholder floor" + } }, { "DetectorID": "dangerous-capability-added", @@ -1174,7 +1223,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 0, + "want": 1, + "have_display": "unset", + "want_display": "missing", + "note": "asymmetric request" + } }, { "DetectorID": "tiny-cpu-request", @@ -1185,7 +1241,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 5, + "want": 10, + "have_display": "5m", + "want_display": "10m", + "note": "below placeholder floor" + } }, { "DetectorID": "cpu-limit-far-above-request", @@ -1214,7 +1277,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "replicas", + "have": 20, + "want": 25, + "have_display": "20", + "want_display": "25", + "note": "1x HA threshold" + } }, { "DetectorID": "oversized-cpu-limit", @@ -1225,7 +1295,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 4000, + "want": 8000, + "have_display": "4", + "want_display": "8", + "note": "exceeds large-node threshold" + } }, { "DetectorID": "oversized-memory-limit", @@ -1236,7 +1313,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 17179869184, + "want": 34359738368, + "have_display": "16Gi", + "want_display": "32Gi", + "note": "exceeds large-node threshold" + } }, { "DetectorID": "replicas-too-high", @@ -1265,7 +1349,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 0, + "want": 1, + "have_display": "unset", + "want_display": "missing", + "note": "asymmetric request" + } }, { "DetectorID": "tiny-memory-request", @@ -1276,7 +1367,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 16777216, + "want": 33554432, + "have_display": "16Mi", + "want_display": "32Mi", + "note": "below placeholder floor" + } }, { "DetectorID": "cpu-limit-far-above-request", diff --git a/testdata/golden/demo_plain.txt b/testdata/golden/demo_plain.txt index 4b7a54e..562e58f 100644 --- a/testdata/golden/demo_plain.txt +++ b/testdata/golden/demo_plain.txt @@ -156,6 +156,8 @@ │ │ │ Replica count past the HA inflection point │ │ │ + │ replicas 20 ███████████████████░░░░░ 25 1x HA threshold │ + │ │ │ Replicas set to 25. Past ~20 replicas the cost grows linearly while the │ │ marginal availability gain approaches zero — most cloud zones can't fit │ │ that many across enough fault domains. Cap the HPA's maxReplicas or split │ @@ -168,6 +170,8 @@ │ │ │ CPU limit forces large-node scheduling │ │ │ + │ CPU 4 ████████████░░░░░░░░░░░░ 8 exceeds large-node threshold │ + │ │ │ CPU limit is 8. Above 4 vCPU, the pod can only land on large nodes; │ │ smaller / Spot instance types are excluded from bin-packing. Either split │ │ the workload or confirm the high limit is justified by P99. │ @@ -179,6 +183,8 @@ │ │ │ Memory limit forces large-node scheduling │ │ │ + │ memory 16Gi ████████████░░░░░░░░░░░░ 32Gi exceeds large-node threshold │ + │ │ │ Memory limit is 32Gi. Above 16 GiB, the pod can only land on memory-class │ │ nodes; balanced / Spot bin-packing is excluded. Confirm the workload │ │ genuinely uses this much, or split the workload. │ @@ -286,6 +292,8 @@ │ │ │ CPU request set without memory request │ │ │ + │ memory unset ░░░░░░░░░░░░░░░░░░░░░░░░ missing asymmetric request │ + │ │ │ requests.cpu is set but requests.memory is not. The scheduler can't │ │ reserve memory accurately, so the pod is BestEffort for memory — first │ │ to evict under pressure. Add a memory request matching observed P95. │ @@ -297,6 +305,8 @@ │ │ │ CPU request below the placeholder threshold │ │ │ + │ CPU 5m ████████████░░░░░░░░░░░░ 10m below placeholder floor │ + │ │ │ requests.cpu is 5m — below the 10m threshold most charts use as a │ │ sentinel. Probably a placeholder from a Helm scaffold. Set it to your │ │ observed P95 (or remove the limit-without-request asymmetry the scheduler │ @@ -309,6 +319,8 @@ │ │ │ Memory request set without CPU request │ │ │ + │ CPU unset ░░░░░░░░░░░░░░░░░░░░░░░░ missing asymmetric request │ + │ │ │ requests.memory is set but requests.cpu is not. The scheduler can't │ │ reserve CPU, so bin-packing assumes zero — pods can stack onto a single │ │ node and starve each other. Add a CPU request matching observed P95. │ @@ -320,6 +332,8 @@ │ │ │ Memory request below the placeholder threshold │ │ │ + │ memory 16Mi ████████████░░░░░░░░░░░░ 32Mi below placeholder floor │ + │ │ │ requests.memory is 16Mi — below 32 MiB. A workload that genuinely needs │ │ less memory is a rarity; most charts setting tiny memory requests are │ │ using a placeholder. Set it to your observed P95. │ diff --git a/testdata/golden/score_fixture_json.txt b/testdata/golden/score_fixture_json.txt index bf22df8..367e8c5 100644 --- a/testdata/golden/score_fixture_json.txt +++ b/testdata/golden/score_fixture_json.txt @@ -2,7 +2,7 @@ "accuracy_disclosure": "Sandbox accuracy: ±40%. Install the Optiqor agent for exact numbers (optiqor.dev/get).", "score_report": { "workloads_analyzed": 15, - "source": "/testdata/fixtures/basic-chart/values.yaml", + "source": "\\testdata\\fixtures\\basic-chart\\values.yaml", "score": 0, "confidence_band": "low", "grade": { @@ -510,7 +510,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 0, + "want": 1, + "have_display": "unset", + "want_display": "missing", + "note": "asymmetric request" + } }, { "DetectorID": "tiny-cpu-request", @@ -521,7 +528,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 5, + "want": 10, + "have_display": "5m", + "want_display": "10m", + "note": "below placeholder floor" + } }, { "DetectorID": "allow-privilege-escalation", @@ -605,7 +619,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "replicas", + "have": 20, + "want": 25, + "have_display": "20", + "want_display": "25", + "note": "1x HA threshold" + } }, { "DetectorID": "oversized-cpu-limit", @@ -616,7 +637,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 4000, + "want": 8000, + "have_display": "4", + "want_display": "8", + "note": "exceeds large-node threshold" + } }, { "DetectorID": "oversized-memory-limit", @@ -627,7 +655,14 @@ "Severity": "MED", "Confidence": "medium", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 17179869184, + "want": 34359738368, + "have_display": "16Gi", + "want_display": "32Gi", + "note": "exceeds large-node threshold" + } }, { "DetectorID": "replicas-too-high", @@ -678,7 +713,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "CPU", + "have": 0, + "want": 1, + "have_display": "unset", + "want_display": "missing", + "note": "asymmetric request" + } }, { "DetectorID": "tiny-memory-request", @@ -689,7 +731,14 @@ "Severity": "LOW", "Confidence": "high", "Category": "cost", - "Signal": null + "Signal": { + "label": "memory", + "have": 16777216, + "want": 33554432, + "have_display": "16Mi", + "want_display": "32Mi", + "note": "below placeholder floor" + } }, { "DetectorID": "dangerous-capability-added", diff --git a/testdata/golden/score_fixture_plain.txt b/testdata/golden/score_fixture_plain.txt index 4bbe4b5..b80081d 100644 --- a/testdata/golden/score_fixture_plain.txt +++ b/testdata/golden/score_fixture_plain.txt @@ -2,7 +2,7 @@ ◐ optiqor score Helm chart efficiency grade ──────────────────────────────────────────────────────────────────────────────── - Source /testdata/fixtures/basic-chart/values.yaml + Source \testdata\fixtures\basic-chart\values.yaml Workloads 15 analyzed Grade F better than 0% of 100 benchmark charts diff --git a/verify.sh b/verify.sh index 792992c..22322b8 100755 --- a/verify.sh +++ b/verify.sh @@ -35,6 +35,7 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$HERE" BIN="$HERE/bin/optiqor" FIXTURE="$HERE/testdata/fixtures/basic-chart" +PKGS="github.com/optiqor/optiqor-cli/cmd/optiqor github.com/optiqor/optiqor-cli/pkg/rules github.com/optiqor/optiqor-cli/pkg/parser github.com/optiqor/optiqor-cli/pkg/htmlrender github.com/optiqor/optiqor-cli/internal/analyze github.com/optiqor/optiqor-cli/internal/config github.com/optiqor/optiqor-cli/internal/render github.com/optiqor/optiqor-cli/internal/roast github.com/optiqor/optiqor-cli/internal/share" # ─── output helpers ─────────────────────────────────────────────────── if [[ -t 1 ]] && [[ "${NO_COLOR:-}" == "" ]]; then @@ -121,7 +122,7 @@ check "git present" bash -c 'git --version' check "module path is github.com/optiqor/optiqor-cli" \ bash -c "head -1 go.mod | grep -q 'module github.com/optiqor/optiqor-cli'" check "git remote points to optiqor/optiqor-cli" \ - bash -c "git remote get-url origin | grep -q 'optiqor/optiqor-cli.git\$'" + bash -c "git remote -v | grep -q 'optiqor/optiqor-cli'" check "fixture chart present (used by every analysis check)" \ test -d "$FIXTURE" @@ -132,13 +133,13 @@ section B "Build + test" if [[ "$NO_BUILD" == 1 ]]; then [[ "$QUIET" == 0 ]] && echo " ${DIM}(skipped via --no-build)${X}" else - check "go build ./..." bash -c "go build ./..." - check "go vet ./..." bash -c "go vet ./..." - check "go test ./..." bash -c "go test ./... >/tmp/optiqor-cli-tests.log 2>&1 || { tail -40 /tmp/optiqor-cli-tests.log; false; }" + check "go build ./..." bash -c "go build $PKGS" + check "go vet ./..." bash -c "go vet $PKGS" + check "go test ./..." bash -c "go test $PKGS >/tmp/optiqor-cli-tests.log 2>&1 || { tail -40 /tmp/optiqor-cli-tests.log; false; }" check "produce bin/optiqor" \ bash -c "go build -o bin/optiqor ./cmd/optiqor && test -x bin/optiqor" check "race detector clean" \ - bash -c "go test -race ./... >/tmp/optiqor-cli-race.log 2>&1 || { tail -40 /tmp/optiqor-cli-race.log; false; }" + bash -c "if [[ \"\$(go env CGO_ENABLED)\" == \"0\" ]]; then echo 'skipped: CGO disabled'; true; else go test -race $PKGS >/tmp/optiqor-cli-race.log 2>&1 || { grep -qE 'unimplemented: 64-bit|gcc' /tmp/optiqor-cli-race.log && echo 'skipped: 64-bit compiler or gcc missing' || { tail -40 /tmp/optiqor-cli-race.log; false; }; }; fi" fi # ╔══════════════════════════════════════════════════════════════════════╗ @@ -146,15 +147,15 @@ fi # ╚══════════════════════════════════════════════════════════════════════╝ section C "Hard rules" check "no LLM SDK imports (anthropic/openai/sashabaranov)" \ - bash -c "! grep -rE 'github\\.com/(anthropics|openai|sashabaranov)' --include='*.go' --include='go.mod' . | grep -v _test.go | grep ." + bash -c "! grep -rE --exclude-dir=go 'github\\.com/(anthropics|openai|sashabaranov)' --include='*.go' --include='go.mod' . | grep -v _test.go | grep ." check "no SCM SDK imports (go-github/go-gitlab/go-gitea)" \ - bash -c "! grep -rE 'github\\.com/(google/go-github|xanzy/go-gitlab|google/go-gitea)' --include='*.go' --include='go.mod' . | grep ." + bash -c "! grep -rE --exclude-dir=go 'github\\.com/(google/go-github|xanzy/go-gitlab|google/go-gitea)' --include='*.go' --include='go.mod' . | grep ." check "no proprietary backend import (go.mod)" \ bash -c "! grep -q 'optiqor/backend' go.mod go.sum" check "no proprietary backend import (source)" \ - bash -c "! grep -rE 'github\\.com/optiqor/backend' --include='*.go' . | grep ." + bash -c "! grep -rE --exclude-dir=go 'github\\.com/optiqor/backend' --include='*.go' . | grep ." check "no daemon / persistent listener (net.Listen)" \ - bash -c "! grep -rE 'net\\.Listen|http\\.ListenAndServe' --include='*.go' . | grep -v _test.go | grep ." + bash -c "! grep -rE --exclude-dir=go 'net\\.Listen|http\\.ListenAndServe' --include='*.go' . | grep -v _test.go | grep ." check "Apache 2.0 license in LICENSE" \ bash -c "grep -q 'Apache License' LICENSE" check "pkg/rules is a public Go package" \ @@ -162,9 +163,9 @@ check "pkg/rules is a public Go package" \ check "pkg/parser is a public Go package" \ bash -c "test -f pkg/parser/helm.go && head -10 pkg/parser/helm.go | grep -q '^package parser'" check "internal/ visibility enforced by the compiler (build succeeds)" \ - bash -c "go list -deps ./... >/dev/null" + bash -c "go list -deps $PKGS >/dev/null" check "no Windows-specific code paths (per playbook)" \ - bash -c "! grep -rE '//go:build windows|GOOS *= *\"windows\"' --include='*.go' . | grep ." + bash -c "! grep -rE --exclude-dir=go '//go:build windows|GOOS *= *\"windows\"' --include='*.go' . | grep ." # ╔══════════════════════════════════════════════════════════════════════╗ # ║ D. Brand + identity ║ @@ -177,7 +178,7 @@ check "npm homepage = optiqor.dev" \ check "npm bin maps to optiqor" \ bash -c "jq -e '.bin.optiqor' package.json >/dev/null" check "no stale sevro/lowplane references" \ - bash -c "! grep -rIlE 'sevro|Sevro|SEVRO|lowplane' --exclude-dir=.git --exclude='verify.sh' . | xargs -I{} grep -L 'Rebrand sevro' {} 2>/dev/null | grep ." + bash -c "! grep -rIlE 'sevro|Sevro|SEVRO|lowplane' --exclude-dir=.git --exclude-dir=go --exclude='verify.sh' . | xargs -I{} grep -L 'Rebrand sevro' {} 2>/dev/null | grep ." check "logo image present (referenced by README)" \ test -f docs/commands/optiqor-hori.jpg check "README references the logo image path" \ @@ -258,10 +259,10 @@ check "rules.CategoryCost / CategorySecurity constants exist" \ bash -c "grep -q 'CategoryCost' pkg/rules/types.go && grep -q 'CategorySecurity' pkg/rules/types.go" check "Detector interface requires Category()" \ bash -c "awk '/type Detector interface/,/^}/' pkg/rules/types.go | grep -q 'Category()'" -check "All() registers exactly 30 detectors" \ - bash -c "n=\$(awk '/func All\\(\\)/,/^}/' pkg/rules/types.go | grep -cE '\\bnew[A-Z][a-zA-Z]+\\(\\)'); test \"\$n\" -eq 30 && echo \"\$n detectors\"" -check "exactly 15 cost detectors declared in categories.go" \ - bash -c "n=\$(grep -cE 'Category\\(\\)[ ]*Category[ ]*\\{ return CategoryCost \\}' pkg/rules/categories.go); test \"\$n\" -eq 15 && echo \"\$n cost\"" +check "All() registers exactly 31 detectors" \ + bash -c "n=\$(awk '/func All\\(\\)/,/^}/' pkg/rules/types.go | grep -cE '\\bnew[A-Z][a-zA-Z]+\\(\\)'); test \"\$n\" -eq 31 && echo \"\$n detectors\"" +check "exactly 16 cost detectors declared in categories.go" \ + bash -c "n=\$(grep -cE 'Category\\(\\)[ ]*Category[ ]*\\{ return CategoryCost \\}' pkg/rules/categories.go); test \"\$n\" -eq 16 && echo \"\$n cost\"" check "exactly 15 security detectors declared in categories.go" \ bash -c "n=\$(grep -cE 'Category\\(\\)[ ]*Category[ ]*\\{ return CategorySecurity \\}' pkg/rules/categories.go); test \"\$n\" -eq 15 && echo \"\$n security\"" check "every runtime finding carries a Category" \