Lesson 188: Sprint Hardening Closure Receipt Against Lesson 173 Budget Variance Gate (2026)

Direct answer: Lesson 183 feeds deficiencies into sprint_hardening_feed. Lesson 173 allocates the sprint budget. Lesson 188 requires a scribe-signed closure receipt per sprint proving closed tickets match that allocation within 25 percentage points—or documents variance in sprint_hardening_budget_variance_note before publish.

Lesson hero for sprint hardening closure receipt against budget variance gate

Why this matters now (2026 H2 closure proof)

Partners stopped accepting “we exported CSV” as sprint discipline:

  • Lesson 183 ships rows; engineering closes tickets ad hoc.
  • Lesson 173 Block 6 asks a yes/no: did work match budget?
  • 2026 H2 readbacks request CLOSURE_RECEIPT.json beside the export SHA-256.
  • Silent drift (35% budget to Lesson 167, 0% closed there) yellow-flags January 2027 replays.

The receipt closes the loop: budget row → closed tickets → measured mix → signed attestation.

Lesson objectives

You will implement:

  • sprint_hardening_closure_receipt — one row per sprint close
  • Mechanical variance calculator (closed mix vs sprint_hardening_budget)
  • Scribe-signed CLOSURE_RECEIPT.json artifact
  • Publish-gate hardening_closure_variance_open
  • Friday Block 6 yes/no metric wiring (Lesson 173)

Prerequisites

  • Lesson 173sprint_hardening_budget, Block 6 variance rule
  • Lesson 183sprint_hardening_feed ingest
  • Lesson 172mock_audit_deficiency_ticket closure statuses
  • Lesson 187 — optional source_red_team_finding_id on closed rows

sprint_hardening_closure_receipt

CREATE TABLE sprint_hardening_closure_receipt (
  receipt_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  sprint_id               TEXT NOT NULL,
  budget_id               UUID NOT NULL REFERENCES sprint_hardening_budget(budget_id),
  cert_window_id          TEXT NOT NULL,
  closed_ticket_count     INT NOT NULL CHECK (closed_ticket_count >= 0),
  closed_mix_json         JSONB NOT NULL,  -- { "167": 0.32, "170": 0.28, ... }
  budget_mix_json         JSONB NOT NULL,  -- snapshot from budget row at close
  max_dimension_delta_pp  NUMERIC(5,2) NOT NULL,  -- percentage points
  within_variance_gate    BOOLEAN NOT NULL,       -- max_delta <= 25
  variance_note           TEXT,
  scribe_email            TEXT NOT NULL,
  signed_at_utc           TIMESTAMPTZ NOT NULL DEFAULT now(),
  receipt_sha256          TEXT NOT NULL,
  UNIQUE (sprint_id)
);

Rule: within_variance_gate = false requires non-empty variance_note (same discipline as Lesson 173).

Variance calculator

For each source_lesson_id in the active budget:

def compute_closure_variance(
    budget_mix: dict[str, float],
    closed_mix: dict[str, float],
) -> tuple[float, bool]:
    deltas = []
    for lesson_id, budget_pct in budget_mix.items():
        closed_pct = closed_mix.get(lesson_id, 0.0)
        deltas.append(abs(closed_pct - budget_pct))
    max_delta_pp = max(deltas) * 100.0
    return max_delta_pp, max_delta_pp <= 25.0

Closed mix counts only tickets with status IN ('resolved', 'carved_out') in the sprint window, grouped by source_lesson_id from Lesson 183 feed rows.

CLOSURE_RECEIPT.json

{
  "schema": "sprint_hardening_closure_receipt_v1",
  "sprint_id": "2026-W42",
  "cert_window_id": "q4_2026_steam_autumn",
  "budget_id": "b7c2…",
  "closed_ticket_count": 14,
  "budget_mix": { "167": 0.35, "170": 0.25, "171": 0.15, "165": 0.10, "reserved": 0.15 },
  "closed_mix": { "167": 0.29, "170": 0.36, "171": 0.14, "165": 0.06, "reserved": 0.15 },
  "max_dimension_delta_pp": 11.0,
  "within_variance_gate": true,
  "variance_note": null,
  "scribe_email": "[email protected]",
  "signed_at_utc": "2026-10-17T16:05:00Z",
  "receipt_sha256": "…"
}

Attach beside Lesson 183 deficiency_export_*.csv in release-evidence/05-operations/.

Publish gate extension

-- block_reason = 'hardening_closure_variance_open'
WHEN EXISTS (
  SELECT 1 FROM sprint_hardening_closure_receipt r
  WHERE r.cert_window_id = :active_cert_window
    AND r.within_variance_gate = false
    AND (r.variance_note IS NULL OR length(trim(r.variance_note)) < 20)
);

Also block when no receipt exists for the sprint that consumed the latest feed ingest (pair with export_run_id on feed metadata).

Friday Block 6 wiring

Input Source
Current sprint_hardening_budget Lesson 173 table
Latest CLOSURE_RECEIPT.json This lesson
New structural-red this week deficiency_trend_board

Yes/no metric (Lesson 173): within_variance_gate = true on the receipt for the sprint under review → answer y. Else n and paste variance_note into the ops doc.

Six-step sprint-close procedure

Step Owner Action
1 Governance owner Confirm feed ingest applied for sprint
2 Engineering Close tickets; no status flips without ticket IDs
3 Scribe Run variance job; review max_dimension_delta_pp
4 Governance owner Approve or write variance_note if > 25 pp
5 Scribe Sign CLOSURE_RECEIPT.json; SHA-256 to Slack
6 Engineering Clear publish gate only when gate satisfied

Target: complete within 30 minutes of Friday Block 6 start.

Common mistakes

  • Counting open tickets as closed — inflates false y on Block 6.
  • Using live budget row after mid-sprint edits — snapshot budget_mix_json at sprint start.
  • Empty variance_note when over 25 pp — publish gate stays red correctly.
  • Receipt for wrong cert_window_id — partner replay mismatch.
  • Skipping receipt when only one ticket closed — still required for audit trail.

Troubleshooting

Symptom Likely cause Fix
Gate hardening_closure_variance_open Over 25 pp, no note Write variance_note ≥ 20 chars
Block 6 always n Closed mix uses wrong lesson IDs Align with source_lesson_id in CSV
Receipt SHA mismatch Manual JSON edit Regenerate from job
Zero closed tickets Sprint not started Skip receipt; document in ops log
Partner asks for proof Only CSV attached Add CLOSURE_RECEIPT.json

Verification checklist

  • [ ] Deliberate > 25 pp drift blocks publish without note
  • [ ] Note ≥ 20 chars clears gate when acknowledged in ticket
  • [ ] Receipt SHA-256 in BUILD_RECEIPT.json
  • [ ] Block 6 yes/no matches within_variance_gate
  • [ ] Closed mix excludes open and carved_back_pending

Mini exercise (25 minutes)

  1. Use Lesson 183 sample CSV (three hot, one structural-red).
  2. Draft budget mix 40% Lesson 167, 30% 170, 30% reserved.
  3. Close tickets: 50% 167, 50% 170 — compute delta.
  4. Write one-sentence variance_note if you force 60% 170.
  5. Export receipt JSON; verify gate behavior.

Continuity

FAQ

Does every sprint need a receipt?
Any sprint that ingested a Lesson 183 export for an active cert window—yes.

Can variance gate be waived?
Only via Lesson 163 carve-out with explicit carve_out_reason referencing receipt ID—not silent override.

What if engineering matched budget but on wrong tickets?
Closed mix still measures lesson IDs; fix ticket tagging, not the receipt math.

Does this replace trend board refresh?
No. Nightly deficiency_recurrence_12w still runs; receipts prove sprint execution, not recurrence analytics.


2026 H2 partners trust spreadsheets when receipts show closed work matched the steering wheel you published in Lesson 173. Sign the closure, pin the SHA-256, and let Block 6 stay a honest yes/no—not a vibe check.