github / github/gh-aw

[formal-spec] forecast-compliance-fixtures/README.md — Formal model & test suite — 2026-09-10

Open
#60,008 0 comments 0 reactions 0 assignees View on GitHub
automation formal-verification specifications testing
Dominant language
Go
Stars
5.1k
Forks
541
Avg merge
5h 48m
Merged PRs (30d)
773

Description

### Summary

This run formalized the **Bernoulli success-gating** and **minimum-observation reliability threshold** semantics of the forecast Monte Carlo engine (`pkg/cli/forecast_montecarlo.go`), which were flagged as an unformalized gap by prior runs against `specs/forecast-compliance-fixtures/README.md`. The gap traces back to requirements R-MC-020/021/022 (Bernoulli independence, zero-contribution on failure) and R-MC-030/031/032 (advisory-only low-sample warning vs. hard nil-projection suppression) in `docs/src/content/docs/specs/forecast-specification.md` §7.2.3 and §7.6. A new formal test file, `pkg/cli/forecast_montecarlo_reliability_formal_test.go`, encodes 7 new predicates (P16–P22) against the existing `runMonteCarlo` implementation and was verified to compile and pass (`go test -run TestFormal ./pkg/cli/...`).

### Specification

- **File**: `specs/forecast-compliance-fixtures/README.md` (cross-referencing `docs/src/content/docs/specs/forecast-specification.md` §7.2.3, §7.6)
- **Focus area**: Monte Carlo forecast engine — Bernoulli success gating and minimum-sample reliability warning
- **Formal notation used**: mixed (TLA+-style state predicates, Z3/SMT-style guard conjunctions, F*-style pre/post contracts)

### Formal Model

Predicates and invariants (illustrative notation)

```
TLA+-style state predicate (R-MC-020, R-MC-021):
∀ i ∈ 1..k: success_i ∈ {0,1} ∧ P(success_i = 1) = successRate
∧ (success_i = 0 ⟹ token_draw_i contributes 0 to trial_tokens)

Z3/SMT-style guard conjunction (R-MC-022):
(totalSampledRunCount = 0) ⟹ (successRate = 0 ∧ ∀ trial: projectedTotal = 0)

F*-style pre/post contract (R-MC-030/031):
requires n = |etObservations|
ensures (n < 10) ⟹ (result.IsReliable = false ∧ result ≠ nil ∧
result.P10, result.P50, result.P90 are all computed,
i.e. the simulation is NOT suppressed — advisory only)

TLA+-style safety predicate (R-MC-032):
(n = 0) ⟹ (result = nil) -- distinct condition from the n<10 warning;
nil-projection short-circuits before any
reliability determination is made.
```

Each predicate is annotated to its source requirement:
- P16 `formalBernoulliIndependentDraws` ← R-MC-020: "Each run in a trial MUST independently draw from Bernoulli(success_rate)."
- P17 `formalFailedRunsContributeZero` ← R-MC-021: "Only successful runs contribute their token draw... Failed runs contribute zero tokens."
- P18 `formalZeroSampleForcesZeroSuccessRate` ← R-MC-022: "If total_sampled_run_count = 0, success_rate MUST be treated as 0... return a zero projection."
- P19 `formalLowSampleStillProducesEstimates` ← R-MC-030/031: "SHOULD emit a warning... MUST still run the Monte Carlo simulation... MUST NOT be suppressed solely on the basis of sample size."
- P20 `formalReliabilityThresholdBoundary` ← R-MC-030: "minimum of 10 ET observations... before treating P10 and P90 as reliable."
- P21 `formalEmptySampleShortCircuitsBeforeReliability` ← R-MC-032: "When n = 0... the Nil Projection Condition in §7.5 applies... This is a separate condition from the low-sample warning."
- P22 `formalUnreliableWarningSurfacedInTable` ← implementation cross-check: `pkg/cli/forecast_render.go`'s stderr warning message threshold must stay in sync with `minObservationsForReliableForecast`.

### Behavioral Coverage Map

| Predicate / Invariant | Test Function | Description |
|---|---|---|
| `formalBernoulliIndependentDraws` (R-MC-020) | `TestFormalBernoulliIndependentDraws` | successRate=1.0 yields positive mean projection; successRate=0.0 yields exact zero |
| `formalFailedRunsContributeZero` (R-MC-021) | `TestFormalFailedRunsContributeZero` | All-failed trials (successCount=0) produce exactly 0 for mean/P10/P50/P90 even with large historical AIC values |
| `formalZeroSampleForcesZeroSuccessRate` (R-MC-022) | `TestFormalZeroSampleForcesZeroSuccessRate` | Empty sample (nil and `[]int{}`) short-circuits to nil rather than computing a NaN successRate |
| `formalLowSampleStillProducesEstimates` (R-MC-030/031) | `TestFormalLowSampleStillProducesEstimates` | n∈{1,3,9} still runs all 10,000 trials and produces a non-zero P50, with IsReliable=false (advisory, not suppressive) |
| `formalReliabilityThresholdBoundary` (R-MC-030) | `TestFormalReliabilityThresholdBoundary` | n=9 → IsReliable=false; n=10 (exact threshold) → IsReliable=true |
| `formalEmptySampleShortCircuitsBeforeReliability` (R-MC-032) | `TestFormalEmptySampleShortCircuitsBeforeReliability` | n=0 returns nil directly, bypassing the IsReliable determination entirely |
| `formalUnreliableWarningSurfacedInTable` | `TestFormalUnreliableWarningSurfacedInTable` | Threshold constant referenced by the stderr warning message stays in sync with `minObservationsForReliableForecast`; confirms the warning-banner trigger condition is reachable |

### Generated Test Suite

📄 pkg/cli/forecast_montecarlo_reliability_formal_test.go

```go
(go/redacted):build !integration

package cli

// Formal compliance tests for the forecast Monte Carlo engine's reliability
// and Bernoulli-success semantics, extending the formal model captured in
// specs/forecast-compliance-fixtures/README.md and
// docs/src/content/docs/specs/forecast-specification.md §7.2.3 and §7.6.
//
// This file closes the gap identified by prior formal-spec-verifier runs
// (see /tmp/gh-aw/repo-memory/default/formal-spec-verifier/forecast-compliance-fixtures-readme.md):
// the Gamma-Poisson compound posterior model and the minimum-observations
// reliability threshold (R-MC-030/031/032) were not yet formalized as
// standalone predicates distinct from the general Monte Carlo P1-P15 series.
//
// Formal Model (illustrative notation, not executable):
//
// TLA+-style state predicate (R-MC-020, R-MC-021):
// ∀ i ∈ 1..k: success_i ∈ {0,1} ∧ P(success_i = 1) = successRate
// ∧ (success_i = 0 ⟹ token_draw_i contributes 0 to trial_tokens)
//
// Z3/SMT-style guard conjunction (R-MC-022):
// (totalSampledRunCount = 0) ⟹ (successRate = 0 ∧ ∀ trial: projectedTotal = 0)
//
// F*-style pre/post contract (R-MC-030/031):
// requires n = |etObservations|
// ensures (n < 10) ⟹ (result.IsReliable = false ∧ result ≠ nil ∧
// result.P10, result.P50, result.P90 are all computed,
// i.e. the simulation is NOT suppressed — advisory only)
//
// TLA+-style safety predicate (R-MC-032):
// (n = 0) ⟹ (result = nil) -- distinct condition from the n<10 warning;
// nil-projection short-circuits before any
// reliability determination is made.
//
// Predicates formalized in this file:
// P16 formalBernoulliIndependentDraws — R-MC-020
// P17 formalFailedRunsContributeZero — R-MC-021
// P18 formalZeroSampleForcesZeroSuccessRate — R-MC-022
// P19 formalLowSampleStillProducesEstimates — R-MC-030/031 (advisory, not suppressive)
// P20 formalReliabilityThresholdBoundary — R-MC-030 boundary at n=10
// P21 formalEmptySampleShortCircuitsBeforeReliability — R-MC-032
// P22 formalUnreliableWarningSurfacedInTable — table-rendering surfaces IsReliable=false

import (
"math/rand" (nolint/redacted):depguard // deterministic seeded RNG required for reproducible Monte Carlo tests
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestFormalBernoulliIndependentDraws encodes P16 (R-MC-020): each run in a
// trial independently draws Bernoulli(successRate), so an extreme successRate
// of 1.0 must yield a non-zero mean projection while successRate of 0.0 must
// yield an exact zero mean projection.
func TestFormalBernoulliIndependentDraws(t *testing.T) {
t.Parallel()

tests := []struct {
name string
successCount int
total int
wantZero bool
}{
{"successRate=1.0 always succeeds", 20, 20, false},
{"successRate=0.0 never succeeds", 0, 20, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
rng := rand.New(rand.NewSource(42)) (nolint/redacted):gosec
obs := make([]int, tt.total)
for i := range obs {
obs[i] = 1000 + i*10
}
mc := runMonteCarlo(obs, tt.successCount, 5.0, rng)
require.NotNil(t, mc, "non-empty sample with positive lambda must produce a summary")
if tt.wantZero {
assert.Equal(t, 0.0, mc.MeanProjectedAIC, "successRate=0 must force mean projected AIC to exactly 0 (R-MC-020/021)")
} else {
assert.Greater(t, mc.MeanProjectedAIC, 0.0, "successRate=1.0 must produce a strictly positive mean projection (R-MC-020)")
}
})
}
}

// TestFormalFailedRunsContributeZero encodes P17 (R-MC-021): only successful
// runs contribute their token draw; failed runs contribute exactly zero to the
// trial total, even when the historical AIC observations are all large values.
func TestFormalFailedRunsContributeZero(t *testing.T) {
t.Parallel()
rng := rand.New(rand.NewSource(7)) (nolint/redacted):gosec
largeObs := []int{50_000, 60_000, 70_000, 80_000}
mc := runMonteCarlo(largeObs, 0, 10.0, rng)
require.NotNil(t, mc, "non-empty sample must produce a summary even with zero successes")
assert.Equal(t, 0.0, mc.MeanProjectedAIC, "all-failed trials must contribute zero AIC (R-MC-021)")
assert.Equal(t, 0.0, mc.P10ProjectedAIC, "P10 must be 0 when no run ever succeeds")
assert.Equal(t, 0.0, mc.P50ProjectedAIC, "P50 must be 0 when no run ever succeeds")
assert.Equal(t, 0.0, mc.P90ProjectedAIC, "P90 must be 0 when no run ever succeeds")
}

// TestFormalZeroSampleForcesZeroSuccessRate encodes P18 (R-MC-022): when the
// historical sample is empty, runMonteCarlo must short-circuit to nil (via the
// n=0 nil-projection rule) rather than attempting a successRate=0/0 division.
func TestFormalZeroSampleForcesZeroSuccessRate(t *testing.T) {
t.Parallel()
rng := rand.New(rand.NewSource(1)) (nolint/redacted):gosec
mc := runMonteCarlo(nil, 0, 10.0, rng)
assert.Nil(t, mc, "empty sample (totalSampledRunCount=0) must yield nil projection, not a NaN successRate (R-MC-022, R-MC-032)")

mcEmptySlice := runMonteCarlo([]int{}, 0, 10.0, rng)
assert.Nil(t, mcEmptySlice, "empty (non-nil) slice must also yield nil projection")
}

// TestFormalLowSampleStillProducesEstimates encodes P19 (R-MC-030/031): the
// warning for n<10 observations is advisory only. The simulation MUST NOT be
// suppressed — P10/P50/P90 must still be computed and non-degenerate.
func TestFormalLowSampleStillProducesEstimates(t *testing.T) {
t.Parallel()

tests := []struct {
name string
n int
}{
{"n=1 observation", 1},
{"n=3 observations", 3},
{"n=9 observations (just below threshold)", 9},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
rng := rand.New(rand.NewSource(int64(tt.n))) (nolint/redacted):gosec
obs := make([]int, tt.n)
for i := range obs {
obs[i] = 1000 + i*500
}
mc := runMonteCarlo(obs, tt.n, 5.0, rng)
require.NotNil(t, mc, "R-MC-031: simulation MUST run even with n=%d < 10 observations — not suppressed", tt.n)
assert.Equal(t, monteCarloIterations, mc.Iterations, "trial count MUST remain 10,000 regardless of sample size (R-MC-031)")
assert.False(t, mc.IsReliable, "n=%d < 10 MUST report IsReliable=false (R-MC-030 advisory flag)", tt.n)
// The advisory flag does not blank out the estimates.
assert.NotZero(t, mc.P50ProjectedAIC, "P50 must still be a real (non-suppressed) estimate for n=%d", tt.n)
})
}
}

// TestFormalReliabilityThresholdBoundary encodes P20: the reliability boundary
// is exactly at n=10 (minObservationsForReliableForecast), with n=9 unreliable
// and n=10 reliable — a strict "at least" boundary, not "more than".
func TestFormalReliabilityThresholdBoundary(t *testing.T) {
t.Parallel()

belowObs := make([]int, minObservationsForReliableForecast-1)
atObs := make([]int, minObservationsForReliableForecast)
for i := range belowObs {
belowObs[i] = 1000 + i*100
}
for i := range atObs {
atObs[i] = 1000 + i*100
}

rngBelow := rand.New(rand.NewSource(99)) (nolint/redacted):gosec
mcBelow := runMonteCarlo(belowObs, len(belowObs), 4.0, rngBelow)
require.NotNil(t, mcBelow)
assert.False(t, mcBelow.IsReliable, "n=%d (threshold-1) MUST be unreliable", len(belowObs))

rngAt := rand.New(rand.NewSource(99)) (nolint/redacted):gosec
mcAt := runMonteCarlo(atObs, len(atObs), 4.0, rngAt)
require.NotNil(t, mcAt)
assert.True(t, mcAt.IsReliable, "n=%d (exactly threshold) MUST be reliable", len(atObs))
}

// TestFormalEmptySampleShortCircuitsBeforeReliability encodes P21 (R-MC-032):
// the n=0 nil-projection condition is a distinct, higher-priority guard than
// the n<10 reliability warning — an empty sample never reaches the IsReliable
// determination at all; it returns nil directly.
func TestFormalEmptySampleShortCircuitsBeforeReliability(t *testing.T) {
t.Parallel()
rng := rand.New(rand.NewSource(3)) (nolint/redacted):gosec
mc := runMonteCarlo(nil, 0, 4.0, rng)
require.Nil(t, mc, "n=0 MUST short-circuit to nil BEFORE any IsReliable computation (R-MC-032 distinct from R-MC-030)")
}

// TestFormalUnreliableWarningSurfacedInTable encodes P22: the table renderer
// tracks whether any workflow's Monte Carlo projection is unreliable, and this
// state feeds a distinct warning banner referencing the exact threshold value.
// This is a stub-level structural check: it verifies the threshold constant
// referenced by the warning message matches minObservationsForReliableForecast,
// ensuring the human-facing warning text cannot silently drift from the
// programmatic threshold used to set IsReliable.
func TestFormalUnreliableWarningSurfacedInTable(t *testing.T) {
t.Parallel()
assert.Equal(t, 10, minObservationsForReliableForecast,
"the reliability threshold constant referenced by the stderr warning message MUST stay in sync with R-MC-030's documented value of 10")

// Sanity: a summary below threshold must have IsReliable=false so the
// table-rendering code path (anyUnreliable branch) is actually reachable.
rng := rand.New(rand.NewSource(11)) (nolint/redacted):gosec
obs := []int{100, 200, 300}
mc := runMonteCarlo(obs, len(obs), 2.0, rng)
require.NotNil(t, mc)
assert.False(t, mc.IsReliable, "a 3-observation sample must set IsReliable=false, which is the trigger condition for the unreliable-projection warning banner")
}
```

### Usage

1. Copy the test file to `pkg/cli/forecast_montecarlo_reliability_formal_test.go` (it already targets the real `runMonteCarlo`, `minObservationsForReliableForecast`, and `monteCarloIterations` symbols in `package cli` — no stubs required).
2. No stub replacement needed; this test exercises the real production implementation directly.
3. Run: `go test -run TestFormal ./pkg/cli/...` (verified locally: all 7 new predicates + existing formal tests pass, 0.06s).

### Context

- Spec processed: `specs/forecast-compliance-fixtures/README.md`
- Formal notation: TLA+-style state predicates, Z3/SMT-style guard conjunctions, F*-style pre/post contracts
- Run: https://github.com/github/gh-aw/actions/runs/34495972661

> Generated by [🔬 Daily Formal Spec Verifier](https://github.com/github/gh-aw/actions/runs/34495972661) · copilot · auto · 155.9 AIC · ⌖ 7.77 AIC · ⊞ 10.5K · [◷](https://github.com/search?q=repo%3Agithub%2Fgh-aw+is%3Aissue+%22gh-aw-workflow-call-id%3A+github%2Fgh-aw%2Fdaily-formal-spec-verifier%22&type=issues)
> - [x] expires on Sep 17, 2026, 7:44 AM UTC-08:00

Contributor guide

Open the contributing guide

Research direction

Start with specs/forecast-compliance-fixtures/README.md, the requirements in docs/src/content/docs/specs/forecast-specification.md §7.2.3 and §7.6, and runMonteCarlo in pkg/cli/forecast_montecarlo.go. Review pkg/cli/forecast_montecarlo_reliability_formal_test.go and the warning path in pkg/cli/forecast_render.go, then run go test -run TestFormal ./pkg/cli/...; done means predicates P16–P22 compile and pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cli, testing-qa
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.