Go to file
type-two 4718548c38 Lane B S18 gates 2+3+1: ⚠️ THE VARIABLE IS POROSITY; the clean boundary is DWELL; the knife
One landing, three gates and Lane C's clock finding. (Not split: sail.js,
rigging.js and balance.test.js each carry work for more than one gate and a
per-hunk split would have been fiction.)

GATE 2 — THE VARIABLE IS `porosity`, AND THE LIAR WAS THE GAME.
`rig.setFabric()` has exactly one call site, `RiggingSession.commit(rig)`, and
the shipped commit path never reaches it: the rigging UI's commit() calls
onCommit and main.js's rigSail goes straight to rig.attach(). So
session.commit(rig) runs in TESTS AND BENCHES ONLY, and the sim's rig (built
once at boot as `new SailRig({anchors})`, porosity defaulting to 0) has flown
the waterproof membrane every night of every playtest while the panel, the
SCORE IT card, the HUD row, the invoice and every audit number said shade
cloth. The F key moved the paperwork and never the physics.

    shade cloth 0.30 (what every card promised)  0/4 lost  p2 3.07  p3 1.78
    membrane 0      (what the sim actually flew)  2/4 lost  p2 3.48  p3 3.90

Row two is D's cold play AND the pin's own _playedReference (hp 55.7, p2 then
p3) to the decimal, reproduced in the shipped game. It also retires C's
"benches under-read the real UI by 5-15%, cause unfound": with the fabric held
equal, the bench and the FULL game chain agree to three decimals, two sites,
two storms. Debris hits on the sail: 0 — D's candidate ruled out by
measurement. Fixed on my side: commit() carries the fabric as onCommit's 4th
argument; dev_rigging.html picks it up, which is the shape of the one-line
pickup filed for A's rigSail.

GATE 3 — THE CLEAN BOUNDARY IS THE FUSE, NOT A PERCENTAGE. Same flight, two
corners, identical $15 shackles, same 3.2 kN rating:
    p1  3.528 kN = 10.3% OVER -> HELD  (over for 0.150 s)
    p2  3.493 kN =  9.2% OVER -> BROKE (over for 0.400 s)
p1 peaks HIGHER and survives, so "peak within X% of rating" is wrong at every
X. sail.js never used peak — it burns a 0.4 s overload fuse. OVERLOAD_SECS /
OVERLOAD_RECOVER are exported and priceCandidate replays them per corner per
tier, so the audit and the sim now answer "will it hold" with one arithmetic.
AUDIT.MARGIN stops deciding anything and says why in its own comment.

GATE 1 — THE RIG YOU DIDN'T BUILD, AND THE KNIFE. `_releaseCorner` is now the
one definition of a corner letting go (it was open-coded twice); on top of it:
`failCorner` for a corner that arrived down, `cutAway` for DESIGN.md line 165
emitting A's requested `{t, corners, reason}`, and `session.inherit()` which
adopts somebody else's rig for $0 and keeps it out of the refund — because
week.js pays back half of every intact corner, so counting a stranger's steel
as yours would have the ledger PUNISHING the player for every corner they cut.
Neither a cut nor an inherited corner emits `break`: warranty must not chase a
corner you chose to let go or one you never hung.

LANE C's CLOCK FINDING, fixed. `_substep` samples the wind at `this.t`, which
attach() zeroes — safe only while the caller's clock starts with it, exactly as
attach()'s own comment says. An emergency opens at t=30, so the cloth would fly
the storm's first 60 s under a sky flying its last 60. attach() now takes
{at, wind} (seeded, or genuinely pre-flown so the drape and the pond are real),
seedClock() is the late door, and `clockSkew` makes any future drift readable
instead of silent — measured 90 N vs 655 N for the same rig entering at t=0 vs
t=40, which is the size of what was hiding.

+15 asserts, all node-verified; the two gate-2 pins mutation-checked both ways.
2026-07-25 19:53:06 +10:00
.claude Sprint 11 integration: merge all lanes, wire leadFor + setBudget, untrack launch.json; Sprint 12 plan + prompts 2026-07-17 18:08:32 +10:00
docs assetcheck/look.html: the verify that answers "is this the right shape" 2026-07-18 16:01:39 +10:00
prototype Add side-systems to design doc + playable sail-storm prototype 2026-07-16 20:55:31 +10:00
tools Lane B S18 gates 2+3+1: ⚠️ THE VARIABLE IS POROSITY; the clean boundary is DWELL; the knife 2026-07-25 19:53:06 +10:00
web/world Lane B S18 gates 2+3+1: ⚠️ THE VARIABLE IS POROSITY; the clean boundary is DWELL; the knife 2026-07-25 19:53:06 +10:00
.gitignore Sprint 11 integration: merge all lanes, wire leadFor + setBudget, untrack launch.json; Sprint 12 plan + prompts 2026-07-17 18:08:32 +10:00
DESIGN.md Add side-systems to design doc + playable sail-storm prototype 2026-07-16 20:55:31 +10:00
LANE_PROMPTS.md SPRINT18 speced: John's ruling (declining costs you), the emergency callout, the glasshouse tier — lane prompts at the tail 2026-07-25 18:45:06 +10:00
PLAN3D.md Correct PLAN3D asset inventory; log M0 contract additions 2026-07-16 21:33:42 +10:00
README.md Merge origin/main: S14 lane integration + review fixes; TEETH becomes SPRINT16 2026-07-19 10:34:21 +10:00
ROADMAP.md Merge origin/main: S14 lane integration + review fixes; TEETH becomes SPRINT16 2026-07-19 10:34:21 +10:00
server.py Vendor three.js r175, add stdlib dev server and launch config 2026-07-16 21:33:42 +10:00
SPRINT2.md Add Sprint 2 assembly instructions: wire proven modules into one playable storm 2026-07-16 22:24:35 +10:00
SPRINT3.md Add Sprint 3 plan and lane prompts: close the game loop 2026-07-17 01:11:16 +10:00
SPRINT4.md Add Sprint 4 plan and lane prompts: face and water 2026-07-17 02:03:47 +10:00
SPRINT5.md Add Sprint 5 plan and lane prompts: hail and the broom 2026-07-17 02:44:30 +10:00
SPRINT6.md Add Sprint 6 plan and lane prompts: winnable, then a week of nights 2026-07-17 08:54:02 +10:00
SPRINT7.md Add Sprint 7 plan and lane prompts: one truth, then the week 2026-07-17 09:48:23 +10:00
SPRINT8.md Add Sprint 8 plan and lane prompts: ship the week 2026-07-17 11:57:51 +10:00
SPRINT9.md Add Sprint 9 plan and lane prompts: pyrrhic call, $80 win, second site 2026-07-17 14:03:45 +10:00
SPRINT10.md Add Sprint 10 plan and lane prompts: sites are data 2026-07-17 15:12:55 +10:00
SPRINT11.md Add Sprint 11 plan and lane prompts: prove the corner block, then the job sheet 2026-07-17 16:44:11 +10:00
SPRINT12.md Sprint 11 integration: merge all lanes, wire leadFor + setBudget, untrack launch.json; Sprint 12 plan + prompts 2026-07-17 18:08:32 +10:00
SPRINT13.md QA pass: fix Enter storm-skip (deployed), log findings; Sprint 13 plan + prompts 2026-07-17 23:26:47 +10:00
SPRINT14.md Sprint 14 plan + prompts: the yard editor — sites are makeable 2026-07-18 14:50:26 +10:00
SPRINT15.md Merge origin/main: S14 lane integration + review fixes; TEETH becomes SPRINT16 2026-07-19 10:34:21 +10:00
SPRINT16.md Merge origin/main: S14 lane integration + review fixes; TEETH becomes SPRINT16 2026-07-19 10:34:21 +10:00
SPRINT17.md Sprint 17 plan + prompts: the book of clients pt 1 — the board; John's seven-night ruling recorded 2026-07-21 00:09:22 +10:00
SPRINT18.md SPRINT18 speced: John's ruling (declining costs you), the emergency callout, the glasshouse tier — lane prompts at the tail 2026-07-25 18:45:06 +10:00
THREADS.md [I] John's ruling (declining costs standing), S18 speced, two-integrator convergence recorded — 522/0/0 2026-07-25 18:51:08 +10:00

HARD YARDS — rig it, then survive the night

(repo/product history: the project was codenamed SHADES through Sprint ~8; the shipped game is HARD YARDS. Internal docs and the gitea repo name still say shades — that's the same thing, not a second game.)

Geometry/tower-defense: you're a small person rigging shade sails against Australian storms. three.js r175 (vendored, no build step) + a zero-dep Python server. Everything the repo needs is in the repo.

Run it

python3 server.py --port 8801     # any free port
# game:     http://localhost:8801/web/world/index.html
# selftest: http://localhost:8801/web/world/selftest.html   (must be all green)
# 2D proto: http://localhost:8801/prototype/index.html      (the original thesis)

No pip, no npm, no build. Python 3.8+ and a browser.

Read in this order

  1. DESIGN.md — the design canon (what the game is)
  2. PLAN3D.md — the build canon (stack, contracts, lane model) · docs/MANUAL.md — the HOW: tool map, asset/site cookbooks, earned gotchas · ROADMAP.md — the growth canon (the arcs from the week to the business)
  3. THREADS.md — the append-only lane log. The project's memory. Read the last few [I] integrator entries to know the current state.
  4. SPRINT15.md — the current sprint (highest-numbered fired sprint wins: SPRINT16 is pre-speced TEETH and fires only when 15 integrates — its own header says so)
  5. LANE_PROMPTS.md — copy-paste prompts per lane, per sprint (bottom = newest)

The lane model (how this repo is built)

Five parallel Opus agents ("lanes"), each in its OWN clone, on its own branch:

lane owns
A main.js, world.js, camera, hud, week, sites, server — shell & integration, merge shepherd
B sail.js, rigging — cloth sim, economy, balance.test pen
C weather, skyfx, debris, storm/site wind data
D player, interact, ladder, broom — and the designated playtester
E Blender asset factory (tools/blender), models/, textures

Hard rules (they exist because each was earned — see THREADS):

  • One clone per lane. Never two agents in one working tree.
  • Rebase onto latest main before starting; small commits; push your branch.
  • File ownership is the merge strategy; cross-lane needs go in THREADS.md.
  • Selftest green after every landing. Skips must be real. An assert that cannot fail is decoration.
  • All sim code is deterministic (dt, t) — no Date.now, no rAF.
  • Measure, don't reason: the repo's biggest bugs were all "a number gathered from the wrong harness". When two harnesses disagree, find the variable.

Setup on a new machine

Gitea's SSH runs over tailscale (the public host is Cloudflare-proxied, no direct SSH). Add to ~/.ssh/config:

Host gitea.partly.party
    HostName 100.71.119.27
    Port 222
    User git

Then:

git clone git@gitea.partly.party:monster/hardyards.git ~/Documents/shades
for L in A B C D E; do
  git clone git@gitea.partly.party:monster/hardyards.git ~/Documents/shades-lane$L
  git -C ~/Documents/shades-lane$L checkout -B lane/$(echo $L | tr 'A-Z' 'a-z') origin/lane/$(echo $L | tr 'A-Z' 'a-z')
done

~/Documents/shades is the integration checkout (main; where merges and sprint docs happen). The five shades-lane* clones are where lane agents run.

Per clone, once: cp .claude/launch.example.json .claude/launch.json and pick a port of your own (integration 8823, lanes AE 88248828). The real launch.json is untracked — six checkouts on one laptop can't share one port.

Sprint cycle

  1. Fire lanes with the newest prompts at the bottom of LANE_PROMPTS.md (Lane A usually first if the sprint gives it rulings/blocking items).
  2. Lanes work, push their branches, log in THREADS.md.
  3. Integrate in the main checkout: merge origin/lane/{e,c,b,d} (THREADS.md conflicts resolve keep-both-chronological; real code conflicts get read), run selftest, play the result, write SPRINT(n+1).md + prompts, push main AND fast-forward all five lane/* branches to main.