Stop Unfreezing Holiday Builds Without a Numbered SHIP Exception List - 2026 Opinion

December 28: #general asks for a menu hitch fix before New Year. You reply "we're frozen." January 2: someone merges anyway because "it's just one line." January 4: January re-entry smoke fails on a binary nobody numbered, nobody smoked, and nobody tied to December defer FREEZE.
January 2027 is when verbal freeze dies. Contractors return, Discord volume spikes, and "just one hotfix" feels compassionate. Without a numbered SHIP exception list, compassionate merges become unfreeze theater—scope creep with holiday branding.
December defer playbook embeds ship_exceptions[] inside december_defer_receipt_v1.json. That is necessary but not sufficient for January discipline: teams need a standalone numbered list auditors can diff, jq can gate, and producers can refuse in public channels without reopening the entire defer table.
This Opinion & Hot Takes piece makes a narrow claim: during holiday freeze and through January re-entry, no binary promotion happens unless the change appears as the next integer in ship_exception_list_receipt_v1.json with smoke proof and label lineage—not Slack approval, not "we'll document later."
Non-repetition note: December defer owns Ship/Freeze/Defer scope table + H1–H6. Visible build label opinion owns player-facing label policy. Developer console opinion owns debug surfaces. January marketing cap #9 owns cash stop rules. This URL owns numbered SHIP exception list discipline + O1–O6 gates when freeze is still culturally "over" but receipts say otherwise.
Pair BUILD_RECEIPT beginner pipeline, Friday capstone row review, jq January reentry pointer, and 14 free January unfreeze audit tools.
Why this matters now (January 2027 / post-freeze hotfix culture)
- Discord unfreeze pressure — Communities treat "frozen" as mood, not receipt; January threads reward fast merges.
- Label drift without numbers — Visible build label discipline fails when hotfixes skip exception rows—players report mystery builds.
- January smoke depends on freeze truth — Re-entry smoke compares to frozen baseline; unnumbered merges invalidate R1–R6 gates.
- Capstone pointer decay — jq January pointer assumes November truth + honest label transition; shadow hotfixes break J4 without a paper trail.
- February RC week proximity — 7-day RC challenge starts from a baseline; unnumbered January patches poison day-one gates.
Direct answer: Maintain ship-exception-list-2027.md with monotonic integers SHIP-001…SHIP-N, file ship_exception_list_receipt_v1.json, set BUILD_RECEIPT ship_exception_list_ok: true, run smoke same day any exception touches player binaries—refuse merges that skip the next number.
The opinion in one paragraph
Holiday freeze is a contract with your future self. If you merge without incrementing a numbered SHIP exception, you did not make a compassionate fix—you made an unlogged promotion that forces January-you to debug folklore. Micro-studios cannot afford folklore during February fest prep. Numbered lists are not bureaucracy; they are the minimum correlation layer between Discord sentiment, BUILD_RECEIPT build_label, and the smoke ritual you claim to respect. Verbal "we're still frozen except this" is how teams accidentally ship three hotfixes and one feature while telling partners zero scope moved.
Who this opinion is for
| Audience | Why read |
|---|---|
| Solo founder moderating Discord | Copy-paste "next SHIP number or no merge" |
| Producer | Block January promote until list matches smoke |
| Engineer | Wire jq gate before holiday CI |
| Community manager | Stop being the informal exception approver |
Beginner path — numbered list in 20 minutes
Copy into release-evidence/ops/ship-exception-list-2027.md:
# Numbered SHIP exception list (holiday freeze → January re-entry)
freeze_start: 2026-12-20
list_owner: producer
next_seq: 3
| Seq | Date | Summary | Touches binary? | build_label_before | build_label_after | Smoke same day? | Owner |
|-----|------|---------|-----------------|--------------------|--------------------|-----------------|-------|
| SHIP-001 | 2026-12-22 | OTLP proxy pin diligence | yes | fest-demo-2026-12-rc3 | fest-demo-2026-12-rc3-hotfix1 | yes | engineering |
| SHIP-002 | 2026-12-29 | Menu null ref P0 | yes | fest-demo-2026-12-rc3-hotfix1 | fest-demo-2026-12-rc3-hotfix2 | yes | engineering |
| SHIP-003 | (reserved) | — | — | — | — | — | — |
Beginner rules:
- Next merge must be
next_seq—no SHIP-004 before SHIP-003 exists. - Binary touch ⇒ smoke same day—no "smoke Monday."
build_label_aftermust update when players receive new bits—pair visible label opinion.- DEFER rows never appear here—only SHIP exceptions during freeze.
Success check: A non-engineer can answer "how many holiday hotfixes shipped?" by reading the highest Seq integer.
Developer path — gates O1–O6
| Gate | Check | Fail action |
|---|---|---|
| O1 | exceptions[] seq integers are 1..N contiguous |
Fix gaps before promote |
| O2 | next_seq == N+1 in markdown matches JSON |
Sync list + receipt |
| O3 | touches_binary: true rows have smoke_receipt_path same day |
Block CI promote |
| O4 | build_label_after on last exception matches BUILD_RECEIPT |
Fix label lineage |
| O5 | ship_exception_list_ok: true on BUILD_RECEIPT |
Fail jq bundle gate |
| O6 | freeze_active true ⇒ no merge without new exception row same commit |
Reject PR |
ship_exception_list_receipt_v1.json (template)
{
"schema": "ship_exception_list_receipt_v1",
"generated_at": "2027-01-02T21:00:00Z",
"freeze_start": "2026-12-20",
"freeze_end": "2027-01-02",
"next_seq": 3,
"exceptions": [
{
"seq": 1,
"id": "SHIP-001",
"date": "2026-12-22",
"summary": "OTLP proxy pin diligence",
"touches_binary": true,
"build_label_before": "fest-demo-2026-12-rc3",
"build_label_after": "fest-demo-2026-12-rc3-hotfix1",
"smoke_required": true,
"smoke_receipt_path": "release-evidence/smoke/SHIP_001_SMOKE.json",
"owner": "engineering",
"december_defer_pointer": "release-evidence/ops/DECEMBER_DEFER_RECEIPT.json"
},
{
"seq": 2,
"id": "SHIP-002",
"date": "2026-12-29",
"summary": "Menu null ref P0",
"touches_binary": true,
"build_label_before": "fest-demo-2026-12-rc3-hotfix1",
"build_label_after": "fest-demo-2026-12-rc3-hotfix2",
"smoke_required": true,
"smoke_receipt_path": "release-evidence/smoke/SHIP_002_SMOKE.json",
"owner": "engineering"
}
],
"pointers": {
"december_defer": "release-evidence/ops/DECEMBER_DEFER_RECEIPT.json",
"january_reentry_smoke": "release-evidence/smoke/JANUARY_REENTRY_SMOKE_RECEIPT.json"
},
"gates": {
"O1": "pass",
"O2": "pass",
"O3": "pass",
"O4": "pass",
"O5": "pass",
"O6": "pass"
},
"ship_exception_list_ok": true
}
Store canonical copy at release-evidence/ops/SHIP_EXCEPTION_LIST_RECEIPT.json.
BUILD_RECEIPT row
{
"ship_exception_list_ok": true,
"ship_exception_count": 2,
"ship_exception_list_receipt": "release-evidence/ops/SHIP_EXCEPTION_LIST_RECEIPT.json",
"build_label": "fest-demo-2026-12-rc3-hotfix2",
"holiday_defer_ok": true
}
Fail-closed: If ship_exception_count ≠ exceptions[].length or last build_label_after ≠ build_label, jq gate fails—prevents "we'll fix the list later."
verify_ship_exception_list.sh
#!/usr/bin/env bash
set -euo pipefail
LIST="${1:-release-evidence/ops/SHIP_EXCEPTION_LIST_RECEIPT.json}"
BR="${2:-BUILD_RECEIPT.json}"
jq -e '.schema == "ship_exception_list_receipt_v1"' "$LIST" || exit 1
# O1 contiguous seq
COUNT=$(jq '.exceptions | length' "$LIST")
for i in $(seq 1 "$COUNT"); do
ACT=$(jq -r --argjson n "$i" '.exceptions[] | select(.seq == $n) | .seq' "$LIST")
[[ "$ACT" == "$i" ]] || { echo "O1 fail: missing seq $i"; exit 2; }
done
# O3 smoke paths when binary touch
jq -e '
[.exceptions[] | select(.touches_binary == true)
| .smoke_receipt_path] | all(. != null and . != "")
' "$LIST" || { echo "O3 fail: binary touch without smoke path"; exit 3; }
LAST=$(jq -r '.exceptions[-1].build_label_after' "$LIST")
BL=$(jq -r '.build_label' "$BR")
[[ "$LAST" == "$BL" ]] || { echo "O4 fail: label mismatch list=$LAST receipt=$BL"; exit 4; }
jq -e '.ship_exception_list_ok == true' "$LIST" || exit 5
echo "ship_exception_list verify: OK"
Exit codes: 1 schema, 2 O1 gaps, 3 O3 smoke, 4 O4 label, 5 O5 latch.
Unfreeze theater vs honest SHIP exception
| Behavior | Theater | Honest exception |
|---|---|---|
| "Just one line" merge | No seq row | SHIP-00N filed same day |
| "We'll smoke after break" | smoke_required ignored | smoke_receipt_path exists |
| Label unchanged after hotfix | Players on mystery build | build_label_after bumps + visible label |
| Slack ✅ from founder | Informal approval | Receipt owner + seq in thread |
| Batch three fixes | One vague exception | Three seq rows or one row with explicit scope hash |
| Reopen combat design | DEFER violation | Move to defer table—not SHIP list |
Producer script for Discord:
Freeze still active per
DECEMBER_DEFER_RECEIPT.json. Binary changes need the next SHIP-00N row inship-exception-list-2027.md, same-day smoke, andbuild_labelbump. Feature ideas go to DEFER—not SHIP.
Relationship to December defer receipt
December defer ship_exceptions[] and this opinion exceptions[] are cousins—not duplicates:
| Field location | Role |
|---|---|
december_defer_receipt_v1.ship_exceptions[] |
Scope authority during Dec 20–Jan 2 freeze window |
ship_exception_list_receipt_v1.exceptions[] |
Numbered audit trail for any binary promotion in freeze + January re-entry |
holiday_defer_ok |
Boolean latch that freeze table was filed |
ship_exception_list_ok |
Boolean latch that every binary change is numbered |
Working dev pattern: On each SHIP merge, bump both receipts in one PR—defer receipt cross-reference pointer + list receipt new seq row.
# Cross-check counts
DEFER_EX=$(jq '.ship_exceptions | length' release-evidence/ops/DECEMBER_DEFER_RECEIPT.json)
LIST_EX=$(jq '.exceptions | length' release-evidence/ops/SHIP_EXCEPTION_LIST_RECEIPT.json)
[[ "$LIST_EX" -ge "$DEFER_EX" ]] || echo "WARN: list shorter than defer exceptions"
January re-entry integration
January re-entry smoke gate R2 frozen-label parity assumes the frozen label is the last SHIP exception's build_label_after or the pre-freeze label if zero exceptions:
| Exception count | frozen_baseline_label source |
|---|---|
| 0 | december_defer build_label_frozen |
| ≥1 | Last exceptions[].build_label_after |
Document in JANUARY_REENTRY_SMOKE_RECEIPT.json notes field—prevents smoke comparing wrong baseline.
Pair January unfreeze audit tools gate U4 re-entry lineage—numbered list is upstream evidence.
jq gate sketch (CI graph)
jq -e '
.ship_exception_list_ok == true
and (.exceptions | length) == (.exceptions | map(.seq) | max)
' release-evidence/ops/SHIP_EXCEPTION_LIST_RECEIPT.json
Run after verify_january_reentry.sh in CI—marketing and pointer gates assume binary truth is numbered.
Proof table
| Row | Evidence | GREEN when |
|---|---|---|
| P1 | DECEMBER_DEFER_RECEIPT.json |
holiday_defer_ok: true |
| P2 | SHIP_EXCEPTION_LIST_RECEIPT.json |
O1–O6 pass |
| P3 | Per-exception smoke JSON | Same calendar date as exception |
| P4 | BUILD_RECEIPT build_label |
Matches last build_label_after |
| P5 | Player-visible label screenshot | Matches BUILD_RECEIPT |
| P6 | JANUARY_REENTRY_SMOKE_RECEIPT.json |
Baseline notes cite SHIP list |
Scenarios A–G
A — Zero exceptions (honest freeze)
exceptions: [], next_seq: 1. January smoke uses December frozen label. Any January merge without SHIP-001 is O6 fail.
B — One P0 hotfix Dec 22
SHIP-001 only. Label hotfix1. Visible label updated. Discord thread links seq row—no further merges until January re-entry plan.
C — "Just config" change that still moves depot
Counts as touches_binary: true—players download new bits. Seq row required; config-only excuse is theater.
D — Art swap in Steam backend only
If no player binary changes, not a SHIP exception—track in marketing defer. If build depot changes, SHIP row required.
E — January 3 merge without list update
Fail O6; block February RC label evening until list reconciled.
F — Partner diligence asks holiday changes
Export numbered list PDF from markdown—faster than reconstructing Git history.
G — Community manager approves hotfix in DMs
Policy: only producer/engineering can assign next seq—CM approval is not O6-compliant.
Engine-specific exception notes (working dev)
Holiday hotfixes look different per stack—the seq row format stays identical:
| Engine | Binary touch signal | Label source to pipe into build_label_after |
|---|---|---|
| Unity | Player build artifact hash changes | Same BUILD_LABEL env as Addressables CI challenge cousins |
| Godot | Export preset output changes | config/version + export template string |
| Unreal | Cooked staging folder changes | PROJECT_VERSION + Insights OTLP cousins if telemetry touched |
| GameMaker | YYZ / zip changes | GM_BUILD_LABEL per setlive evening |
| Ren'Py | game/ bytecode bundle changes |
config.version per VN freeze challenge |
Rule: If CI publishes a new depot ID, you need a seq row—even when the diff is "one script."
Git tag pairing (optional but valuable)
Tag ship-001-20261222 at the exception merge commit. jq can verify tag exists:
git tag -l 'ship-*' | wc -l
# should be >= exception count in receipt
Tags are not a substitute for JSON receipts—they are human-readable anchors for partners browsing Git without opening release-evidence/.
January week-by-week enforcement calendar
| Week | Policy emphasis |
|---|---|
| Dec 20–Jan 2 | O6 strict—freeze window; any binary ⇒ new seq same day |
| Jan 3–5 | Last SHIP label becomes re-entry smoke baseline; no unnumbered "cleanup" merges |
| Jan 6–12 | Contractors return—DEFER feature work; SHIP only for P0 |
| Jan 13–19 | February RC label week—list must be GREEN before label evening |
| Jan 20+ | Archive list to release-evidence/ops/archive/ after Monday capstone reconciliation forward #11 |
Counterarguments (and rebuttals)
"Numbered lists slow P0 fixes."
SHIP-001 takes five minutes—debugging unnumbered January smoke takes five hours.
"We're two people—we trust each other."
Trust does not help players report which build crashed—visible label needs lineage.
"December defer already has ship_exceptions."
Embed is scope; standalone numbered list is audit diff friendly for January partners and jq.
"Freeze ended Jan 2—why list still?"
January re-entry week still compares against freeze baseline; list closes the narrative through first Thursday row review.
"Exceptions are embarrassing."
Embarrassing is silent drift—numbered lists show discipline.
Engineering floor + marketing cap coupling
December engineering floor caps hours for SHIP work—each exception should include estimated_engineering_h in notes so blackout caps are not silently blown.
January marketing cap SR2 can reference ship_exception_list_ok—pause paid social if list jq fails while store still promises stable demo.
Printable producer card
## SHIP exception quick gate (pin in #engineering)
1. Is freeze/active re-entry policy still on? → defer table link
2. Next seq: SHIP-___ from ship-exception-list-2027.md
3. Touches binary? → smoke today + label bump
4. Update SHIP_EXCEPTION_LIST_RECEIPT.json + BUILD_RECEIPT same PR
5. Run verify_ship_exception_list.sh → paste exit 0
6. Discord reply with seq id—not "merged!"
Common mistakes
Using SHIP list for DEFER items — Combat rebalance belongs in defer table, not exception integers.
Skipping numbers "because revert" — Reverts still get seq rows; never reuse integers.
One exception row for five files — Scope hash or split rows—auditors read seq as atomic promotions.
Smoke next week — O3 is same calendar day; overnight is fine, next sprint is not.
Label bump without visible UI — Policy violation per visible label opinion—exception incomplete.
Merging on Jan 3 before re-entry smoke — Invalidates baseline story; run smoke on last SHIP label first.
Outbound references
- Semantic Versioning discipline — cousin thinking for monotonic identifiers (not semver for demos—monotonic seq instead)
- Steamworks branches — depot promotion truth behind labels
Related GamineAI reads
- December BUILD_RECEIPT defer list
- Visible build label opinion
- January re-entry Wednesday smoke
- jq January reentry capstone pointer
- 14 free January unfreeze audit tools
- December engineering floor cap
- January marketing spend cap
- Friday capstone row review
- Thursday BUILD_RECEIPT row review
- 7-day February RC smoke challenge
Key takeaways
- Verbal freeze is theater—numbered SHIP-001…N rows are the minimum honest unfreeze log.
ship_exception_list_receipt_v1.jsongates O1–O6; standalone from but paired with December defer.- Binary touch ⇒ same-day smoke—no January "we'll verify later."
build_label_afteron the last exception must match BUILD_RECEIPT + visible label.ship_exception_list_okis a separate latch fromholiday_defer_ok.- Discord "just one hotfix" is the enemy—reply with next seq or DEFER table.
- January re-entry smoke baseline comes from last SHIP label or frozen label if zero exceptions.
verify_ship_exception_list.shgives CI one exit code for holiday promotion discipline.- Producers pin the quick gate card—engineers should not be informal exception approvers.
- Monday capstone reconciliation expects numbered list GREEN before February upload.
- January marketing cap can pause ads when list jq fails.
- Live fest week: HOTFIX exception opinion continues numbered discipline with HOTFIX-001…N—cousin to SHIP, distinct schema.
- Zero exceptions is valid—any unlogged merge is not.
FAQ
Is this replacing December defer ship_exceptions?
No—pair both in the same PR; defer owns scope window, list owns numbered audit trail.
Do store-only changes need SHIP numbers?
Only when player binaries change; metadata-only may be marketing defer—document honestly.
What if we need SHIP-003 and SHIP-004 same day?
Allowed—ints must stay contiguous; two smokes if both touch binaries.
Can contractors assign seq numbers?
Only if named owner in receipt policy—default engineering lead + producer sign-off.
Does freeze ending Jan 2 retire the list?
Keep through January re-entry smoke and first Thursday row review—then archive to release-evidence/ops/archive/.
How does this relate to visible build label opinion?
Every build_label_after bump requires player-visible update—exceptions without label visibility are incomplete.
What about non-Steam demos?
Same policy—itch/HTML5 SKUs need channel suffix labels per GX resource cousins.