Lesson 267: Discord Playtest Thread CSV Ingest Receipt on BUILD_RECEIPT (2026)
Direct answer: Every Tuesday before Wednesday smoke, promote playtest_csv_ingest_receipt_v1.json with D1–D6 gates—archive Discord CSV + sha256, column map, jq normalize to playtest_feedback_normalized.json, build_id crosswalk to BUILD_RECEIPT, P0/P1 blocker counts, and wednesday_smoke_allowed policy. Pair Discord CSV jq preflight (Guide #29), Tuesday CSV ingest ritual, and cousin Lesson 256 (UTF-8 Weblate bytes—not triage counts).

Why this matters now (async playtest before Wednesday smoke)
October–November 2026 Discord threads produce 30–80 CSV rows per fest week while stand-ups stay fifteen minutes. Producers promote nextfest-nov-2026-rc5 with four P0 rows still referencing last Tuesday’s build_id. Wednesday smoke proves binary golden path—it does not read facilitator sheets. Lesson 266 wires VOD→GitHub honesty; 267 wires async CSV→BUILD_RECEIPT scope before promotion.
The Tuesday twelve-minute ritual is facilitator rhythm; 267 is the BUILD_RECEIPT milestone.
Beginner path (Tuesday twelve-minute ingest)
| Step | Action | Success check |
|---|---|---|
| 1 | Export Discord thread CSV to playtest-feedback/raw/ |
D1 sha256 logged |
| 2 | Confirm column map matches headers | D2 pass |
| 3 | Run jq normalize script | D3 playtest_feedback_normalized.json |
| 4 | Crosswalk build_id to BUILD_RECEIPT |
D4 single match |
| 5 | Count P0/P1; apply promotion policy | D5 wednesday_smoke_allowed |
| 6 | File receipt + BUILD_RECEIPT columns | csv_ingest_ok: true |
Time: ~52 minutes first Tuesday setup; ~12 minutes when jq script + column map are pinned.
Developer path (gates D1–D6)
| Gate | Check | Fail when |
|---|---|---|
| D1 | Discord CSV archived | Path missing or sha256 not logged |
| D2 | Column map matches README | Required fields absent after alias map |
| D3 | jq normalize runs clean | Empty rows[] or jq exit non-zero |
| D4 | build_id crosswalk BUILD_RECEIPT |
Multiple builds or Discord pin ≠ receipt |
| D5 | P0/P1 counts + promotion policy | P0 > 0 without waiver when promoting |
| D6 | playtest_csv_ingest_receipt_v1.json |
csv_ingest_ok false at promote |
Align with Guide #29 preflight—blog = rhythm; guide = ninety-second gate; 267 = BUILD_RECEIPT column.
D1 — Archive raw CSV + sha256
CSV="playtest-feedback/raw/discord_thread_2026-11-04.csv"
test -f "$CSV"
sha256sum "$CSV" | tee playtest-feedback/raw/discord_thread_2026-11-04.sha256
PowerShell:
Get-FileHash "playtest-feedback\raw\discord_thread_2026-11-04.csv" -Algorithm SHA256
Rule: Archive whatever facilitator exports—encoding fixes are Lesson 256 cousin lane, not D1.
D2 — playtest_csv_column_map_v1.json
{
"schema": "playtest_csv_column_map_v1",
"source_headers": ["Build", "P-level", "Category", "Issue", "Link"],
"canonical_map": {
"build_id": "Build",
"severity": "P-level",
"area": "Category",
"summary": "Issue",
"discord_message_url": "Link"
},
"severity_normalize": {
"P0": ["P0", "p0", "Blocker", "Critical"],
"P1": ["P1", "p1", "High"],
"P2": ["P2", "p2", "Medium"]
}
}
Fail-closed: facilitator adds columns mid-week without map update → D2 fail before jq.
D3 — jq normalize
jq -n --slurpfile rows playtest-feedback/raw/discord_thread_2026-11-04.csv \
--argfile map playtest-feedback/playtest_csv_column_map_v1.json \
-f scripts/normalize_playtest_csv.jq \
> playtest-feedback/normalized/playtest_feedback_normalized.json
Normalized output shape:
{
"schema": "playtest_feedback_normalized_v1",
"build_label": "2026-11-04-playtest-rc4",
"rows": [
{
"build_id": "2026-11-04-playtest-rc4",
"severity": "P0",
"area": "progression",
"summary": "Soft-lock after boss door",
"discord_message_url": "https://discord.com/channels/…"
}
],
"counts": { "P0": 1, "P1": 3, "P2": 8, "total": 12 }
}
D4 — build_id crosswalk
BUILD="$(jq -r '.build_id' release-evidence/BUILD_RECEIPT.json)"
UNIQUE="$(jq -r '.rows[].build_id' playtest-feedback/normalized/playtest_feedback_normalized.json | sort -u)"
test "$UNIQUE" = "$BUILD"
Fail: split CSV by build; re-run D3 per file. Visible build label helps players confirm the same string.
D5 — blocker promotion policy
| Count | wednesday_smoke_allowed |
Promotion rule |
|---|---|---|
| P0 > 0 | false unless waiver |
Block fest promotion until triaged |
| P1 > 5 | true with p1_overflow: true |
YELLOW—document in receipt notes |
| P2 only | true |
GREEN for smoke scope (still file receipt) |
P0 waiver (when you must promote anyway):
## P0 waiver roster
| build_id | P0 summary | approver | UTC | waiver_id |
|----------|------------|----------|-----|-----------|
| 2026-11-04-playtest-rc4 | progression soft-lock | producer@studio | 2026-11-05T08:00:00Z | WAIVER-2026-11-04-01 |
Receipt field: "p0_waiver_id": "WAIVER-2026-11-04-01" — Thursday row review column waiver Y/N.
D6 — playtest_csv_ingest_receipt_v1.json
{
"schema": "playtest_csv_ingest_receipt_v1",
"build_label": "2026-11-04-playtest-rc4",
"source_csv_path": "playtest-feedback/raw/discord_thread_2026-11-04.csv",
"source_csv_sha256": "cc99a435…",
"normalized_path": "playtest-feedback/normalized/playtest_feedback_normalized.json",
"column_map_path": "playtest-feedback/playtest_csv_column_map_v1.json",
"counts": { "P0": 1, "P1": 3, "P2": 8, "total": 12 },
"p0_blockers": ["progression: Soft-lock after boss door"],
"cousin_receipts": {
"weblate_utf8_handoff": "release-evidence/localization/WEBLATE_CSV_UTF8_HANDOFF_RECEIPT.json",
"whisper_issue_tag": "release-evidence/playtest/whisper-tags/WHISPER_ISSUE_TAG_RECEIPT_fragment_03.json"
},
"gates": {
"D1_csv_archived": "pass",
"D2_column_map": "pass",
"D3_jq_normalize": "pass",
"D4_build_id_crosswalk": "pass",
"D5_blocker_counts": "pass",
"D6_receipt": "pass"
},
"csv_ingest_ok": true,
"wednesday_smoke_allowed": true
}
Path: release-evidence/playtest/PLAYTEST_CSV_INGEST_RECEIPT.json.
C1 — cousin receipt crosswalk
| Field | Cousin (256) | Cousin (266) | This lesson (267) |
|---|---|---|---|
| Schema | weblate_csv_utf8_handoff_receipt_v1 |
whisper_issue_tag_receipt_v1 |
playtest_csv_ingest_receipt_v1 |
| Scope | UTF-8 Weblate bytes | VOD→GitHub quotes | Discord CSV P0/P1 scope |
| Day | After Tuesday ingest | Post-Whisper | Tuesday before Wednesday |
Order: 267 jq ingest → 256 UTF-8 handoff (if Weblate) → 266 Whisper tags (per clip) → Wednesday smoke.
D6 — BUILD_RECEIPT promotion jq
RECEIPT="release-evidence/playtest/PLAYTEST_CSV_INGEST_RECEIPT.json"
jq -e '.csv_ingest_ok == true' "$RECEIPT"
jq -e '.gates.D4_build_id_crosswalk == "pass"' "$RECEIPT"
jq -e '.wednesday_smoke_allowed == true' "$RECEIPT"
BUILD_RECEIPT columns:
| Column | Example |
|---|---|
playtest_csv_p0 |
1 |
playtest_csv_p1 |
3 |
csv_ingest_ok |
true |
csv_ingest_receipt_path |
release-evidence/playtest/PLAYTEST_CSV_INGEST_RECEIPT.json |
Mental model — triage vs encoding vs smoke
| Stage | Owner | Proves |
|---|---|---|
| 267 (Tuesday) | jq + P0/P1 counts | Async scope on BUILD_RECEIPT |
| 256 | UTF-8 handoff | Weblate-safe bytes |
| 266 | Whisper tags | Quote-only GitHub issues |
| Wednesday smoke | S4–S6 | Installed demo boots |
Key takeaways
- Tuesday CSV ingest is not optional—Wednesday smoke does not read Discord sheets.
- Column map before jq—header drift silently empties
rows[](D2). build_idonly from BUILD_RECEIPT (D4)—not thread title memory.- P0 blocks promotion unless documented waiver (D5).
- Guide #29 preflight — ninety-second gate before this lesson.
- Tuesday ritual blog owns facilitator timer; 267 owns BUILD_RECEIPT promotion.
- 18 free async playtest tools — facilitator README templates.
- Cousin: Lesson 268 forward — CI cancel poison lane.
- Lesson 256 — encoding handoff after counts are honest.
- Thursday row review — diff
playtest_csv_p0week over week.
Common mistakes
- Promoting Wednesday without filing D6 receipt.
- Excel double-click save → UTF-16—fix in 256, but D1 still archives raw export.
- jq script pinned to old headers after facilitator renames column (D2).
- Counting P0 from Slack reactions instead of normalized JSON (D5).
- Merging
playtest_csv_ingest_receiptintoweblate_csv_utf8_handoff_receipt. - Setting
wednesday_smoke_allowed: truewith open P0 and no waiver.
Troubleshooting
| Symptom | Lane |
|---|---|
jq returns empty rows[] |
D2 column map |
Two build_id values in CSV |
Split files; D4 |
| P0 in sheet, receipt shows 0 | Severity alias missing in map |
| Weblate mojibake after ingest | Lesson 256 |
| VOD issues without CSV row | Lesson 266 parallel lane |
Mini exercise (45 minutes)
- Export sample Discord CSV—log D1 sha256.
- Intentionally rename a header—confirm D2 fail.
- Fix map; run jq—verify counts.
- Crosswalk
build_id(D4). - Add one P0 row—confirm
wednesday_smoke_allowed: falsewithout waiver. - File receipt; update BUILD_RECEIPT columns for Thursday review.
Continuity — November 2026 fest close-out + playtest triage (266–276)
| Lesson | Receipt focus |
|---|---|
| 266 | Whisper issue tag |
| 267 (this) | Discord CSV ingest |
| 268 | Unity CI cancel poison (forward) |
Previous: Lesson 266 — Whisper playtest issue tag receipt
Next: Lesson 269 — Godot export VDF LF line ending steamcmd receipt
FAQ
Same as the Tuesday ritual blog?
Blog owns twelve-minute facilitator rhythm; 267 is the course milestone with BUILD_RECEIPT promotion jq.
Same as Guide #29?
Guide = ninety-second preflight; 267 = receipt culture + cousin crosswalk to 256/266.
Same as Lesson 256?
256 = UTF-8 Weblate encoding handoff; 267 = P0/P1 triage counts from Discord CSV.
Can we skip Tuesday if Whisper tags filed?
No—VOD tags and async CSV answer different questions; wire both receipts when both lanes run.
Google Sheets instead of Discord export?
Fine for D1—column map must match exported headers.
Does this replace Wednesday smoke?
No—binary smoke is separate; 267 sets scope honesty before smoke runs.
November promotion lies when P0 rows live only in Discord—jq normalize Tuesday, crosswalk build_id, file blocker counts, then run Wednesday smoke.