W83 — Buildings Get Dirty, Wear Out, and Break - #348
Merged
Conversation
Implements cleanliness-wear (system 14) for real: systems 1, 4, and 11 now defer their building_meter_delta effects to scratch instead of applying them immediately, and system 14 sums the service/litter/staff/policy sources once per building/meter before clamping once, per 20-contract.md §9 and §4.16. Wear reaching zero now breaks an open or closed building; cleanliness alone never does. The contract's fifth ordered source, `incident`, has no independent mechanism distinct from `staff`'s deferred onResolve effect and is left a documented no-op placeholder — see design/90-decisions.md.
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
…t event severity cleanlinessWear's clamp reused effects.ts's safeAdd/clamp instead of a bare Math.max/min, restoring the overflow guard every other meter-delta path relies on. building.status.changed now emits at debug everywhere, matching the existing setBuildingOpen call site (severity is fixed per event name, not per call site). Also drops the redundant buildings-array re-walk in favor of the byId map's own preserved iteration order.
Reject building_meter_delta in objective/failure effect lists and in a duration-bearing incident's onResolve: those paths run after system 14 and never defer, so a delta reaching them would double-clamp and could never trigger the broken transition. Reject initialWear: 0 on a building definition too, since a building created already at the wear floor can never re-trigger that transition either. Both were flagged in review as latent, content-dependent gaps rather than live bugs.
This was referenced Aug 20, 2026
world-graph: deferred building-meter effects mark 'applied' before system 14 composes the total
#349
Open
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
The-Running-Dev
commented
Aug 20, 2026
Four review findings from the cleanliness-wear pass:
- `cleanliness-wear` accumulated deferred deltas with unchecked `+` while
every peer accumulator uses `safeAdd`, and `building_meter_delta.delta`
is unvalidated. Three sources summing across 2**53 rounded the
intermediate and left the meter silently off by one, which the final
clamp cannot detect. Now checked at the point precision is lost, with a
test that fails when the fix is reverted.
- pipeline.ts held four raw NUL bytes as map-key separators, so file(1)
classified it as binary and grep reported no matches without -a. The
same separator is written as an escape in effects.ts and changes.ts.
Two of the four came in with W82; escaping only the W83 pair would have
left the file binary, so all four are escaped. Identical at runtime.
- `invalid_initial_wear` guards `<= 0` but its message claimed the value
was zero, which misleads on a negative wear.
- `incident` is the contract's third ordered slot, not the fifth, in both
the system doc comment and the 90-decisions entry.
Two contract-level findings are reported on the PR and deliberately not
applied here, since both need a contract amendment rather than a review
edit: `undeferrable_building_meter_effect` rejects content 20-contract.md
§9.2 permits ("Systems after 14 apply their own group locally"), and
`building_broken` is an eleventh `visible: true` audit reason where the
audit table states there are ten.
W83's guard forbade every `building_meter_delta` on the effect lists owned
by systems that run after 14 and never defer to it. The reasoning held for
`wear` and not for `cleanliness`, and as written it contradicted §9.2's own
"Systems after 14 apply their own group locally": an objective reward as
ordinary as `onCompleted: [{ building_meter_delta, cleanliness, +20 }]`
failed the whole campaign.
Only `wear` has a status transition hanging off it, so only `wear` can be
silently wrong when applied late — it clamps independently and can never
reach §4.16's `broken`. `cleanliness` has no transition, so a late delta is
merely clamped locally, which is what §9.2 already describes.
- `forbidBuildingMeterDelta` becomes `forbidUndeferrableWearDelta` and
matches on `meter === "wear"`.
- §9.2 states the rule and names the three lists it covers.
- 90-decisions records the decision and what the two alternatives cost.
- Tests assert exact counts both ways: the wear variant produces 3
rejections, the cleanliness variant 0. Verified by widening the guard
back, which fails the positive case.
Two stale counts corrected in the generated guide while regenerating it:
both said five of the twenty world-graph tick systems are known-and-retained
gaps. W81, W82 and W83 each reduced that and none updated the guide; the
register now stands at two, one no-op (`alerts`) and one partial
(`incidents`). The digest gate does not check content, so neither was caught.
The-Running-Dev
enabled auto-merge (squash)
August 20, 2026 20:32
W83 shipped three codes the contract never listed. `building_broken` is the sharp one: it rides on a `visible: true` record, and §13's audit table opened with "All ten" while enumerating exactly that many, so the count was wrong the moment W83 landed. `invalid_initial_wear` and `undeferrable_building_meter_effect` were missing from the rejection table alongside peers like `invalid_inventory`. - Rejection table gains both validator codes at tier 1. - Audit table gains `building_broken` and the count is re-derived from the rows rather than incremented — eleven. - §13's note on why this class of omission survives every gate now records that W83 was its second occurrence. The deeper fix — a test that fails when a `visible: true` reason is missing from the audit table — is deliberately not here. §13 already says nothing checks emitted against registered, and the manual control that replaces it is now stated in the open register so /track can raise it as its own unit. Parsing a markdown table from a test is a new kind of coupling and wants deciding on its own, not as a side effect of a review pass.
After W83 every effect list has a building-meter rule except `incidents[].onStart`, which is declared, shape-validated and dead — no system applies it. W47 makes it live, and the rule depends on a choice W47 owns: if `onStart` runs only for system 16's rolls it lands after system 14 and a wear delta there could never reach the broken transition, so §9.2's rejection should extend to it; if W47 also applies `onStart` at the `start_incident` sites in systems 1 and 4, those run before 14 and could defer legitimately, so extending it would forbid content W47 wants. Both readings are defensible, so the review pass left the code alone and recorded the question instead. The entry also notes the contract's own MVP example puts a cleanliness delta in `onStart`, not a wear one, so the wear-only rule would not contradict it either way.
W83 added a validator rejecting `initialWear <= 0`, because a building created at the wear floor can never re-enter the broken transition — the meter never changes, so the check never fires. The type comment beside the field still read `integer 0..100`, so the contract advertised a value the validator rejects. Raised on an earlier review thread and left open there as a design call. The design call has since been made and shipped; this is the contract catching up to it.
…sion ConvertTo-HumanDocumentation.ps1 was not run after editing 90-decisions.md's marked block in 427ecd9, leaving the generated page stale and failing Test-Documentation.ps1's drift check.
This was referenced Aug 20, 2026
The-Running-Dev
added a commit
that referenced
this pull request
Aug 21, 2026
## Summary - **Branch cleanup**: deleted two remote branches fully merged into main (`feature/sync-main-status-b5d31e`, `slice/W78`); left `feature/bulgaria-arcs-expansion`, `slice/S1` (closed, unmerged PRs) and `reconcile/w71-w76-tracking` (open PR #316) alone per user decision. No local branches were eligible for deletion. - **Track sync**: marked W78-W83 done in `design/30-slices.md`, linking their closing issues (#326-#331) — all six had already merged (PRs #340, #342, #343, #345, #347, #348) but the tracker still showed "Not started". - **CLAUDE.md**: retired the stale `qodo-code-review` reference in the required-checks note now that automated PR review runs via `anthropics/claude-code-action` (Qodo is retired). - **Kit sync**: fast-forwarded `~/.agent-kit` from `af610a6` to `e9e21bf` (9 commits) and reconciled command cores/tools into this repo. `verify.md`'s core was `Unmigrated-Blocked` (this repo's gate table had no companion); resolved by moving the table into `.claude/commands/verify-local.md` under the `gate-commands` category the core declares, recorded in `design/90-decisions.md`. - **Regenerated human docs**: the tracker sync left `engine/TODO.md` and its compatibility pointer stale against `design/30-slices.md`; `build/Test-Documentation.ps1` caught it, so `./build/ConvertTo-HumanDocumentation.ps1` was re-run and the output committed. Also refreshed `verify-local.md`'s gate table — `verify.yml` no longer flags `Test-Companion.ps1`/`Test-DesignState.ps1` as CI gates, discovered while re-deriving the gate list from the workflow files rather than trusting the memorized one. ### Verified Ran and passed: - Typecheck — `npm --prefix src/engine run typecheck`, exit 0 - Lint — `npm --prefix src/engine run lint`, exit 0 - Test — `npm --prefix src/engine test` (vitest) — 79/79 test files, 1127/1127 tests - Pack package — `npm pack --silent` produced `the-running-dev-game-engine-0.10.0.tgz` - Inspect tarball — no `src/`, no `tsconfig*.json`, no `.test.*` entries; `package/dist/` present - Consumer smoke — clean install/build/smoke against the packed tarball - Parse-check PowerShell scripts — every `tools/*.ps1` parses clean - Validate Markdown links, terminology, and generated files — `./build/Test-Documentation.ps1`: drift check passed across 18 generated engine pages, compatibility pointers and the guide; 123 Markdown files checked - Build and verify landing page — `npm --prefix src/engine run build` + `npm --prefix site run check` (format/lint/typecheck/test/browser/build/merge) all green Ran and failed: - Run Pester tests — `Invoke-Pester -Path tools`: 241/253 passed, 12 failed. All 12 are in the newly kit-synced `Read-DesignState.Tests.ps1`/`Test-DesignState.Tests.ps1`/`Test-CIWorkflow.Tests.ps1`/`Update-DesignProjection.Tests.ps1` — self-referential tests (`Split-Path $PSScriptRoot -Parent`) that validate the agent-kit's own dogfooded `design/state/` per-unit-record mechanism and its `verify.yml` shape against whatever repo they're copied into. This repo's `design/` uses `00-brief`/`10-design`/`20-contract`/`30-slices`/`90-decisions`, not the kit's `design/state/` scheme, so these fail structurally (`StateSetAbsent`) rather than from a regression — the same gap `Test-DesignState.ps1` itself reports as exit 2 when run directly here. Full failing-test list and detail in `.claude/verify-report.json`. Did not run: - Build documentation — `docs.ps1` is absent from this checkout (CLAUDE.md: installer-generated via `Invoke-SetupDocs`, not committed); needs Docker and an installed `docs.ps1`, neither set up this session - Merge landing page into documentation build — depends on the `artifacts/docs` output of "Build documentation" above, which didn't run - Test the host — `dotnet test` needs `NUGET_GITHUB_TOKEN` for the sibling-repo NuGet feed, not set locally - Positive route and probe smoke — needs a built host image, blocked on the same missing token - Negative fixture (corrupted artifact must fail to start) — same --- <details><summary><b>Agent detail</b></summary> <!-- agent:start --> - **Not a slice** — housekeeping (branch cleanup, `/kit-sync`, tracker sync) plus the doc-drift fix it surfaced - **`.claude/verify-report.json`**: validated by `tools/Test-VerifyReport.ps1` (`Valid`, 15 gates, 0 findings, 0 could-not-evaluate) before this section was rendered from it - **Left undone:** the kit's self-referential Pester tests (12 failures) — whether they should be scoped out of non-dogfooding targets is an open question, not resolved here <!-- agent:end --> </details>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed, and why. Wires
cleanliness-wear(world-graph system 14) for real: systems 1,4, and 11 now defer their
building_meter_deltaeffects to scratch instead of applying themimmediately, and system 14 sums the service/litter/staff/policy sources once per building/meter,
in contract order, before clamping once — replacing the prior implementation that applied only
one of five delta sources and never moved wear at all. Wear reaching zero now breaks an open or
closed building; cleanliness reaching zero does not, on its own. The contract's fifth ordered
source,
incident, has no mechanism independent ofstaff's deferredonResolveeffect and isleft a documented no-op placeholder (
design/90-decisions.md).Verified
Ran and passed:
npm --prefix src/engine run typechecknpm --prefix src/engine run lintnpm --prefix src/engine test— 79 files, 1121 tests passednpm pack --silentproduced the tarball with no errorssrc/, notsconfig*.json, no.test.*;package/dist/present (309 entries)node dist/smoke.jsexited 0./build/Test-Documentation.ps1passed across 18 generated engine pages and 123 Markdown filesnpm --prefix site run check(lint, typecheck, unit, browser, build, merge) all passedDid not run:
docs.ps1is not present in this checkout (generated byInvoke-SetupDocs, not committed); a Docker host-image build was also attempted and its docs stage was cancelled when a sibling stage failed for an unrelated reason (below). Deferred todocs-ci.yml's "Build documentation" check on this PR.artifacts/docs, which the above did not produce locally. Deferred todocs-ci.ymlon this PR.dotnet testfor the host needsNUGET_GITHUB_TOKENfor the sibling-repo NuGet feed, not set locally. Deferred tohost-image.ymlon this PR.dotnet restoreofSubZeroDev.Platform.Hostingfailed locally withValue cannot be null or empty string. (Parameter 'username')because no NuGet credentials are present in this environment. Deferred tohost-image.ymlon this PR.host-image.ymlon this PR.Agent detail
design/30-slices.md§ W83 @cbc0d1bworld-graph-mvp-cleaning) was added rather than an existing outcome moving, so there is nothing to record as an intended change.