Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/campaign-build-exclusions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Campaigns in the pinned content repo that `tools/build-every-campaign.py`
# does NOT require to build, each with the reason it may not.
#
# WHY THIS FILE IS NOT A SKIP LIST
#
# A silently skipped campaign is a vacuous green (CLAUDE.md: a green gate that
# binds to nothing is VACUOUS, not a pass) and is worse than no gate at all. So
# an entry here does not switch a campaign off — it INVERTS the assertion and
# pins it to a named failure:
#
# * the campaign is still built, on every run;
# * it must still fail, and fail with EXACTLY the codes in `expect_codes` —
# any other error code is a NEW break that would otherwise have hidden
# behind the exclusion, and reds the job;
# * if it starts BUILDING, that reds the job too. The exclusion has expired
# and must be deleted in the PR that fixed the campaign, or the next
# regression it suffers would be excused by a stale line in this file.
#
# So the honest states are two, both visible in the job's own output: "built"
# and "known-red for this reason, still exactly this reason".
#
# Fields
# id campaign directory name under <content>/campaigns/
# task the tracking item that will retire this entry
# reason why it cannot build today, in one sentence
# expect_codes the complete set of DW error codes the build must emit

[[exclusion]]
id = "hollow-vigil"
task = "#34"
reason = """
Two dialogue option labels are authored over the 146 px a vanilla dialog button \
fits (DW0331); vanilla scrolls an over-wide caption back and forth rather than \
wrapping it, so the fix is a content rewrite of the labels, not an engine change. \
Out of scope for the PR that introduced this gate — the gate exists to stop \
ENGINE changes from breaking campaigns, and hollow-vigil was already red before \
it.\
"""
expect_codes = ["DW0331"]
4 changes: 3 additions & 1 deletion .github/required-status-checks.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# The exact `name:` of every CI job that is a REQUIRED status check on `main`
# (owner decision, 2026-08-05: all ten, previously three; eleven since
# ADR-0017 added the cross-build shelf).
# ADR-0017 added the cross-build shelf, twelve since task #44 added the
# campaign-build gate).
#
# THIS FILE IS A DEADLOCK TRIPWIRE. Branch protection matches a required context
# by its NAME STRING. Rename a job in ci.yml and its required context stops
Expand Down Expand Up @@ -30,4 +31,5 @@ skin toolchain (pytest)
i18n translation tool (pytest)
mecha cross-check (PR only)
campaign storybooks (engine-version marker)
campaign builds (every campaign in the content repo)
tier 2 (datapack load + PackTest)
48 changes: 47 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ jobs:
# Deterministic, offline, stdlib-only python3.
- name: no early-exit consumer on the right of a pipe (SIGPIPE + pipefail)
run: python3 tools/check-shell-pipe-shortcircuit.py
# All ten jobs are required status checks (owner decision 2026-08-05; it was
# EVERY job here is a required status check (owner decision 2026-08-05; it was
# three, so `tier 2` — datapack load plus the whole PackTest suite — never
# blocked a merge). Branch protection matches a required context by its NAME
# STRING: rename a job and its context stops reporting forever, blocking
Expand Down Expand Up @@ -362,6 +362,52 @@ jobs:
- name: every campaign storybook carries a TRUE engine-version marker
run: python3 tools/check-storybook-version.py

# Every gate above this line builds a FIXTURE. Nothing in CI had ever built a
# real campaign, so the product could stop compiling with the whole board
# green: PR #260 reached 10/10 while stopping the flagship released campaign
# `nobodys-cave-island` from building at all (26 × `DW0364` on standable cells
# at the ocean line). Fixtures exercise one verb each; a campaign is where the
# verbs meet a real prefab library, a real layout solve and a real translation
# sidecar, and that is the surface an engine change breaks.
#
# Campaigns are DISCOVERED in the pinned content checkout, never listed here,
# so a new campaign on content `main` is gated by the next re-pin with nobody
# remembering. A campaign that cannot build today is recorded in
# `.github/campaign-build-exclusions.toml`, which INVERTS the assertion instead
# of removing it — the campaign is still built and must still fail with exactly
# the codes recorded there, so neither a new break hiding behind an exclusion
# nor an exclusion that has expired can pass. Every run states its binding
# count (CLAUDE.md: a green gate that binds to nothing is VACUOUS).
#
# Its own job, not a step in `rust`: a campaign build is minutes of layout
# solving, and burying it inside the fmt/clippy/test job would make one status
# answer two unrelated questions. On every push, not PR-only — a change that
# breaks a released campaign must not be able to sit on a green board for the
# window between push and PR.
campaign-builds:
# Name must stay exactly "campaign builds (every campaign in the content
# repo)" — it is a required status context in branch protection; renaming it
# deadlocks every PR (see .github/required-status-checks.txt).
name: campaign builds (every campaign in the content repo)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# LFS on (the default): a campaign build copies the prefab `.nbt` BODIES
# into the datapack, so pointers would fail here for a reason that has
# nothing to do with the engine change under test.
- uses: ./.github/actions/checkout-content
- uses: Swatinem/rust-cache@v2
with: { key: campaign-builds }
# Built once and handed to the driver, rather than a `cargo run` per
# campaign per language: the gate's subject is WHICH engine built the
# campaign, so the binary is named explicitly and the script never infers
# one. Dev profile — the workspace sets `opt-level = 1` for it (PR #313),
# which is what makes a real layout solve affordable on every push.
- name: build the engine under test
run: cargo build -p delvec --bin delvec
- name: every campaign in the pinned content repo still builds
run: python3 tools/build-every-campaign.py --delvec target/debug/delvec

# ------------------------------------------------------------------ tier 2 --
# spec-0004 tier 2 (every PR): build the delve, prove it loads with zero errors
# on the pinned vanilla server, and run the generated PackTest suite headless.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ Never shipped inside a delve.
|---|---|---|
| `tools/i18n-translate.py` | agent | `python3 tools/i18n-translate.py <campaign-dir> --lang <code> [--config f] [--delvec cmd] [--batch-size n] [--dry-run] [--force] [--no-validate] [--reflect\|--no-reflect]` — external OpenAI-compatible API, generation-time only; `--reflect` runs the three-step translate → critique → revise pass; see [`i18n.md`](i18n.md) |
| `tools/skin/` (`delve_skin`) | agent | `python -m delve_skin all <cast.json> --skins-dir D --catalog-dir D --preview-dir D [--id ID] [--scale N]`, or the `build` / `preview` / `catalog` stages individually. Needs its own venv (`pip install -r tools/skin/requirements.txt`); see [`../../tools/skin/README.md`](../../tools/skin/README.md) |
| `tools/build-every-campaign.py` | CI + agent (run it before proposing any engine change that touches emission, layout or validation) | `python3 tools/build-every-campaign.py --delvec <binary> [--content <checkout>]` — builds **every campaign** the pinned content repo carries, in **every language its `world.json` declares**, and reds if one stops building. Closes the gap that let PR #260 reach 10/10 green while stopping the flagship released campaign `nobodys-cave-island` from building at all (26 × `DW0364`): every other gate builds a FIXTURE, and a fixture exercises one verb, where a campaign is the only place the verbs meet a real prefab library, a real layout solve and a real translation sidecar. Campaigns are **discovered** (any dir under `<content>/campaigns/` with a `world.json`), never listed, so the next content re-pin gates a new campaign with nobody remembering. `--delvec` is required and never inferred — the gate's whole subject is *which engine* built the campaign. A campaign that cannot build today goes in `.github/campaign-build-exclusions.toml`, which **inverts** the assertion rather than removing it: still built, must still fail, and must fail with **exactly** the recorded `expect_codes` — an extra code is a new break that was hiding behind the exclusion, and a SUCCESS is an expired exclusion, both red. Currently one entry: `hollow-vigil`, `DW0331` (task #34). States its binding count every run (discovered / built green / known-red, each named); discovering zero campaigns, building zero campaigns, or an exclusion naming a campaign that no longer exists are each a red. Runs in CI as `campaign builds (every campaign in the content repo)`, on every push |
| `tools/check-dw-codes.py` | CI | `python3 tools/check-dw-codes.py` — asserts the DW catalog in `compiler.md` matches `crates/**/*.rs` both ways, and that every code has a test |
| `tools/check-effect-roots.py` | CI | `python3 tools/check-effect-roots.py` — no source file may enumerate the campaign's **effect roots** by hand. An effect root is a `Vec<QuestEffect>` emission can lower; there are five, four hang off the quests stage and the fifth off dialogue, and nothing about the DSL's shape makes them findable by inspection — so every walk that needed "every effect" was written by someone enumerating the roots they knew about. Six were found and fixed independently; a sweep found thirteen more; this gate, on the run that introduced it, found three the sweep had missed (`continuity::excluded_npcs`, `emit::first_damage_players`, `emit_v04_packtests`' despawn scan). **None was ever red** — a walk that visits four of five roots looks correct over any campaign that does not use the fifth. `delvewright_dsl::effects` is now the one enumeration and every walk inherits it, which closes the thirteen; this gate is what stops a fourteenth, since the root fields are ordinary public fields and no type can forbid the loop. Flags a window of 40 source lines naming **3+ distinct** roots, outside an allowlist that carries a REASON per entry (the enumeration itself; `validate::reserved_v06_world`, sound by construction; `plan::required_anchors_for_area`, an open finding printed on every run). **Known non-proof, stated in its docstring**: a proximity heuristic over text — roots spread across a hundred lines, or reached through a helper taking the list as an argument, are invisible. States its binding count every run (currently 128 files, 92 markers); **examining zero files or finding zero markers is a red**, because a renamed field would otherwise leave it quietly green forever. Runs as a step of `docs (local link check)` — a step, not a job, since every job name is a required status context |
| `tools/check-doc-dupes.py` | CI | `python3 tools/check-doc-dupes.py [path …]` — merge-artifact gate over `docs/**/*.md` + `README.md`: no two body rows in one markdown table share a first-cell key, no heading repeats within a file, no git conflict markers. Kills the class that put `shortcuts[]` in the stage-5 table twice (owner finding 2026-08-03). Same-key rows in *different* tables are fine; a genuine same-table collision means restructure the table, not allowlist it |
Expand Down
Loading
Loading