Skip to content

Latest commit

 

History

1,372 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

elevator-sim

A tower-management sim with a real engine underneath.

Design a building — floors, elevator banks, security zones — fill it with people who have somewhere to be, and watch the morning rush hit. Then change something and find out whether it actually helped.

The twist is that nothing here is faked for effect. The cars obey jerk and acceleration limits, the crowds arrive in batches with weights the load sensor can feel, and when you ask "did that help?" the answer comes back with a confidence interval — or an honest refusal, if the run cannot support one. It plays like a game and it measures like an instrument.

The loop

  1. Build — pick a tower or author your own: floors, banks, car specs, service and access zones.
  2. Populate — choose a traffic pattern. Morning up-peak, lunch two-way, evening down-peak, or your own demand template.
  3. Run the day — watch cars fill, doors dwell, queues build on the floors that are struggling.
  4. Change one thing — a dispatcher, a weight, an extra car, a faster motor.
  5. Race them — the same passengers, to the second, against both configurations. The simulator tells you whether the difference is real or whether you are looking at noise.

Step 5 is the whole point. The nearest comparator this project has examined at source — Elevator Saga — publishes an average wait computed only over riders who were delivered, unconditionally, from a single unseeded run. This simulator will not publish that figure: censored is one of the five grounds on which it suppresses a mean outright (packages/core/src/metrics/awtValidity.ts), and it will not name a winner from one replication.

(That sentence used to read "it is where most tower sims quietly cheat". No tower sim was ever checked for it — the one verified instance is a programming toy — so the claim was narrowed to what the evidence supports. The teardown that found it is docs/24-competitive-teardown.md § 8.)

Why it is a hard game to build honestly

Elevator group control is a genuinely hard scheduling problem: continuous state space, partially observable, non-stationary demand, hard physical constraints. It is also one where the obvious improvement frequently fails to show up in the data — a faster elevator can measurably increase average waiting time if the gain is smaller than the statistical noise. Buy the upgrade, watch the number get worse, conclude something false.

A sim that wants to be satisfying will round that away and give you the win. This one will not.

How much you can trust the numbers

The short version: this simulator is built to be hard to fool, including by the people writing it. Most of the engineering effort here has gone into refusing to say things that aren't supported rather than into producing more output. If you have ever been handed a traffic study whose conclusion evaporated when someone re-ran it, this section is about why that is difficult to do here.

Read it as the rules of the game. They are unusually strict, and the strictness is the feature.

1. It declines to answer when it can't answer honestly

A simulated run that saturates — where the passenger queue grows faster than the group can clear it — has an average waiting time, arithmetically. That number is meaningless: it tells you when you stopped watching, not how the building performs. The simulator suppresses it and prints the reason instead.

Saturation is only one of four grounds for refusing to publish a mean. The others are an empty reporting window (nobody was served), censoring above the unserved limit, and a journey past the 900-second abandonment horizon — the last of which was added after a run was found publishing a tidy mean beside a 922.7-second wait. The queue had grown enormously and drained just in time; the trend test saw a queue that had stopped growing, the censoring test saw one that had cleared, and neither saw the disaster in between.

In practice this refusal bites hard. Across the twelve shipped dispatchers and five shipped buildings at the viewer's default settings, only 14 of 60 combinations produce a quotable average waiting time. The honest response to that is not to relax the rule. It is to show the observations that are valid — people carried, longest wait, queue depth per floor — and say plainly why the average is missing.

2. Every comparison races the same passengers

When two dispatchers are compared, they are not run against "similar" traffic. They are run against identical passengers — the same people arriving at the same second on the same floors wanting the same destinations — so that any difference in the result is the dispatcher and not the luck of the draw. This is common random numbers, and — measured here rather than taken from the literature — it is worth between 1.8× and 324× in the number of runs required, depending on how similar the two dispatchers are. The published general figure is a single number; the honest answer is that it depends enormously on what you are comparing, and budgeting by it blindly will under-run the comparisons that need the most care.

Because the pairing is what makes the comparison sensitive, it is checked rather than assumed: both arms' passenger traces are compared field by field, every replication, and a comparison whose pairing has broken refuses to report an interval at all.

Results are reported as paired confidence intervals — the interval on the difference, not two separate intervals side by side. Comparing two overlapping intervals and concluding "no significant difference" is a standard and seductive error, and the codebase forbids it in writing.

3. Some differences are too small to see, and we say which

Below a certain size, a difference cannot be distinguished from noise at any realistic number of runs. This simulator measures that floor rather than assuming it, and reports anything underneath it as below the resolution limit — never as a win.

The floor is not one number. For two dispatchers whose cost weightings are near neighbours it is around 0.20 seconds; for structurally different dispatchers it is roughly ten times coarser. And because those figures were originally measured on waiting time, they were re-measured directly on door-to-door journey time before being used to judge journey-time results — which turned out to make the test stricter to pass, not easier.

There is a third case, and it is the uncomfortable one: an effect whose required number of runs exceeds the point's own saturation ceiling is permanently unresolvable there. Not under-budgeted — unresolvable. The simulator says so rather than quoting a number from an under-powered run.

4. Checked against the closed-form answer

Under pure up-peak, the simulated round-trip time, interval and handling capacity are checked against the Barney / CIBSE closed-form round-trip-time calculation — the hand calculation a lift consultant would do — on all five shipped buildings. If the simulation and the closed form disagree, the working assumption is that the simulation is wrong until proven otherwise.

Reference values — capacities, door times, jerk and acceleration limits, passenger mass, arrival rates by building type — come from CIBSE Guide D, ISO 8100-32 and the published lift-engineering literature, and changing one requires citing why.

5. The physics is the boring kind of correct

The details that are easy to skip are the ones that change conclusions:

  • Cars fill to 80 % of rated capacity, not 100 %. Using the nameplate figure makes every result systematically optimistic.
  • Motion is modelled with jerk and acceleration limits, so a short hop never reaches rated speed. A simulator that ignores this concludes that faster lifts always help. They don't.
  • Passenger mass is a distribution, not an average — otherwise the load weighing device has nothing to weigh, and bypass behaviour becomes fiction.
  • Passengers arrive in batches, because people travel together, and batch size materially changes loading and stopping patterns.
  • Service zoning, access control and operational zoning are three different things and are never collapsed into one field: which floors a shaft physically reaches, which floors a credential permits, and how the controller chooses to partition the building are separate questions with separate failure modes.

6. The question is written down before the answer exists

Acceptance criteria for each phase are committed before the code that answers them, and the commit history shows the ordering. A criterion written after a result is indistinguishable from one fitted to it.

The project's own working rule is blunt: do not weaken an acceptance criterion to make a phase pass — raise it instead. That has been honoured in the awkward direction. One improvement was written exactly as the backlog requested, measured, and then thrown away, because the measurement showed the change would have hidden a live tuning dimension from the optimiser. A gate is a claim about the world, and the wrong claim costs more than no claim.

7. Negative results are published, not buried

Learned dispatcher control was implemented, measured across eight pre-registered operating points spanning five buildings and several traffic patterns, and not accepted — the improvement was smaller than what the apparatus can resolve. Two of those points cleared the statistical bar and were still refused, because the effect was a third to a half of the smallest difference detectable there.

More usefully, the reason is now known rather than guessed: the shipped demand model varies how busy the building is over time, but never varies the mix of up, down and interfloor traffic within a run — so the condition a pattern-switching controller exists to exploit does not occur at any shipped operating point. That is a far more actionable answer than a green tick would have been.

8. Published numbers are re-derived, not retyped

Three figures in this repository once failed to reproduce from the code that was supposed to produce them — one measured before a feature was wired and never regenerated, two mis-copied through a double rounding — and nothing noticed, because no test re-derived a published interval. Now they do: the headline figures are re-computed from the runs that produced them, and a change that moves one turns the test suite red instead of quietly changing what the project claims.

9. A test nobody has watched fail is not yet a test

The standing practice is to break a behaviour deliberately and confirm the guard goes red before trusting it. This is not ceremony. In one recent stretch of work it caught six tests that could not fail, by five different mechanisms — including a display test whose fixture routed it silently past the code path it named, and, one level up, a checking tool that reported "no failures" for every case because a command-line flag it depended on had been renamed. It would have certified a dead test suite as fully live.

10. Where a caller can get it wrong, we try to make it impossible rather than merely detected

The practices above are about the numbers. This one is about the code that produces them, and it is the newest.

A function that refuses a bad argument by failing loudly is doing the right thing — but "the right behaviour on a bad call" is not the same as "the bad call cannot be made". The routine that scores one simulated run against one design goal used to refuse two of the seven goal types, correctly, because those two compare two configurations and no single run can answer them. Four places in the codebase checked for that before calling it. All four were right, and nothing obliged them to be — the check was a convention four authors held by having read the same comment. So the restriction is now part of the type the function accepts: a caller that has not checked no longer fails at run time, it fails to compile. Three modules and two dozen test lines stopped building the moment that landed, which is how we know the change has teeth rather than merely reads well.

The same reasoning fixed how the browser viewer reports a page it cannot start on. It resolves 73 named elements, and it used to stop at the first one missing — including, awkwardly, the element it writes its own error messages into, so the one situation where the message mattered most was the one where nobody ever saw it. It now names every missing element at once, and the list of what a page must contain is a document rather than 73 calls buried in a 1,600-line file.

Neither change fixed a bug. Both removed a way for the next person to introduce one.

What this does not claim

It is a simulation, not a building. The people in it are demand, not characters: they arrive, they wait, they board, they leave. Today it does not model passenger psychology, lift-lobby crowd flow, or anyone deciding to take the stairs — a rider will queue for twenty minutes without ever glancing at the stairwell, which no real person does.

That gap is the difference between a good engine and a building you believe in, and closing it is active work rather than an aspiration — see Goals. The constraint it has to respect is the one that governs every model change here: new behaviour arrives opt-in and off by default, so a run that does not ask for it is bit-identical to the run before it existed. A traffic-model change that silently moved a published figure would invalidate far more than the feature was worth.

Where a limitation is known it is written down in docs/07-handoff.md § 8 with its measurement rather than left for you to discover — including the ones that are inconvenient.

Goals

Shipped:

  • Configurable buildings — arbitrary floors, multiple banks, service zones, access-control zones
  • Realistic physics — S-curve motion profiles with acceleration and jerk limits, door timing, load weighing
  • Realistic traffic — office / residential / hotel / mixed-use arrival profiles with peak templates
  • Pluggable dispatchers — nearest-car, ETA, zoned, auction-based and destination-dispatch, all as weight vectors in data/, not classes
  • Statistically valid results — multi-replication runs, common random numbers, sequential confidence-interval stopping

In design — the building-behaviour program. These are what turn a correct engine into a tower you believe in, and each is scoped to arrive opt-in and off by default so no shipped figure moves:

  • Richer traffic variance — an independent traffic seed so demand can be re-rolled without disturbing anything else, an authored body-mass distribution, a group-size curve you can shape, and day-to-day variability so Tuesday is not a copy of Monday
  • Passenger behaviour — patience and abandonment, lift-lobby crowding that slows boarding when a lobby is packed, and stair-taking with the asymmetry real people have (a flight down is cheap, a flight up is not)
  • A learned dispatcher you can teach — the surface for training a policy against your own building and traffic, with the same acceptance bar every other dispatcher faces. Measured three times so far and refused three times (§ D145, § D156, § D200) — the refusals are published above the wins

Target smart behaviors

Behavior Where it lives
Predictive pre-positioning / parking Group controller + learned arrival predictor
Hall-call bypass when car is full Car load sensor → dispatcher reassignment
Parallel service of heavy floors Group controller demand splitting
Dynamic zoning under up-peak Group controller policy
Access-control-aware routing Building ACL → dispatcher feasibility filter

Documentation

Doc Contents
Project Brief Vision, scope, non-goals, success criteria
Project Charter The game layer's charter — vision, player promise, the five design pillars a player-facing pull request is held against, the two audiences and what each must get in the first session, success criteria S1–S10 each written so it can fail, and the non-goals that are as load-bearing as the pillars. Complements the project brief rather than amending it: the brief is a research brief and uses the word game zero times, which is how playability came to be excluded from Phase 9's criterion as unfalsifiable. Carries its own corrections table, including one figure it withdrew rather than published — 194 KiB and 198 kB are the same file — and is explicit that only pillar 3's wording is directly attested
Audiences and the core loop Who the two audiences are, what each must get in the first session with an observable condition that can fail, and the core loop every mode declares which half it serves. Written against premises that did not survive verification — packages/viz/UX.md already defined five roles and the design-canonical gameplay handoff already carried a per-mode loop table with lengths and lose-conditions — so it reconciles those rather than writing a second inventory beside them, which is the drift this repository exists to prevent. Carries the honest register of modes that do not currently serve the loop
Competitive teardown Nine adjacent games read against one template, each with an adopt-or-refuse verdict tied to a named pillar or non-goal. Extends the experience-layer contract § 3's cited prior-art survey rather than restating it — two accounts of the same four titles would drift — and adds the five that appear nowhere in the tree, Elevator Saga first, since it is the closest comparator and the one this product is most likely to be mistaken for. Opens with the correction note that both of the issue's premises failed verification
Telemetry and privacy The posture first, then the schema, because data collected without a posture cannot be un-collected: four KPIs and nothing else, each tied to the charter criterion it serves and each estimator chosen so event loss makes the gate harder rather than easier. Its load-bearing choice is the run pointer rather than the payload — invariant 5 makes a run reconstructible from seed and config, so an event names the run instead of carrying derived figures, which answers more later while saying less about a person now. Carries what is deliberately not collected (including a refusal itself, whose cost is published beside every figure rather than fixed by collecting more), and states plainly that two criteria are not funnel-measurable at all and get no proxy
Flow maps Every player-facing flow end to end, and each one in five states rather than one: happy path, empty, restored after a reload, server-unavailable, and recovery. A flow mapped only on its happy path is not mapped — the two worst defects this project has produced were both found by playing rather than by the suite, and both lived in the restored and recovery columns. The graph is derived from everyday/actionBar.ts's rows and the screen registry rather than asserted, so the next reader can re-derive it; dead ends are listed and split into unreachable-by-defect and unreachable-by-design
Audio direction The audio decision, and it is a recommended cut that was overruled§ D344 ruled on 2026-08-24 that audio ships, speed-tiered, and #258 is the lane that builds it. The argument is measured rather than aesthetic, and the measurement is what survived the ruling: a door cycle of 9.8 simulated seconds is 0.33 s of wall time at the stage's default speed and 16 ms at its fastest, so door chimes and motor whine cannot exist as discrete cues up there. What did not survive is the sentence that made it a cut — the slowest rung was 8× compressed when this was written and is real time, the same cycle in 9.8 s of it — since § D354, which is why the owner read the same arithmetic as a defect report about the speed ladder. Carries the strongest argument against its own recommendation in full, exactly what the cut changes across five sites, and the three things that would reopen it — one of which has since been delivered
Support matrix Which browsers and platforms are supported, in four tiers, with the reason each is in the tier it is — and the gap between what is tested and what would be supported as the document's main content, since today that is Chromium only, on two desktop OSes, and no touch device at all. Touch is committed as best effort and split into two clauses that can each fail: small-screen layout at 360 px and above is in scope for launch, touch interaction is not. Carries a three-part performance budget for charter S9 written to be enforceable on a CI runner that is not a mid-range laptop, and the costed answer to what testing the matrix would take — because a matrix nobody can afford to test quietly becomes a lie
Playtest programme Recruitment, cadence and protocol — the document M2 cannot exit without, since ten testers complete the slice and six of ten can say what went wrong are unexecutable without one. It is also the only instrument for the two success criteria telemetry can never measure. Its governing rule is the one this project learned the expensive way: a playtest finding is a claim, not a defect — findings route through verification before they are scheduled, and a refuted one is recorded with its refutation rather than dropped
Art direction The visual language, stated rather than invented — the vendored handoff is canonical for the interface, so this names what exists, records where the build diverges and why, and specifies only what is genuinely unowned. That is chiefly the stage, where the crowd, doors and queues were all already drawn and the real defect was that a shut car was painted entirely over by its own door leaves — fixed, and the fix is asserted on painted pixels rather than described. Carries the ceiling on pillar P3: the stage may make a thing visible as it happens and may not publish the run's summary of it
Game design What the player does, what progression and the economy mean, and what losing looks like in each mode — the questions twenty-three engineering contracts specify around without ever answering. It references those contracts rather than restating them, since two accounts of one rule can drift apart, and carries a register of every contract it relies on with confirmation that none is weakened
Difficulty curve What the player is expected to understand at each campaign stage, each week and each fix case, what pressure produces it, and what has to go wrong for the lesson to land. Bounded by § D345 and written against the world in which that decision's fix has landed — one bar per stage whatever tier is selected — because a curve specified against the current code would bake in the thing the decision forbids. Its measurements refuted the brief's own central figure: three of ten campaign stages clear from the dispatcher dropdown alone, not four, and three more stages satisfy that rule only because they admit no dispatcher but their own. Specifies the sweep that re-derives all of it, since the published count has now gone stale three times with nothing checking it
Usability audit of the rules Asks of this repository's own decisions and invariants whether any of them is costing a player something, and separates a rule that is wrong from a rule that is right and has been read one step too far. Six findings, every claim about what a player meets taken by driving the built page at 1280 x 720 rather than read off a docstring — two of the findings are docstrings that were wrong. No rule was weakened and none is proposed for weakening: one was right and unenforceable and is now enforced (fourteen dead controls carrying no reason, taken to zero), two are right and are escalated because a lane may not relax them, one issue's premise is refuted as already fixed, and the audit's own opening hypothesis — that the suppression rules leave a player with a bare withheld — is refuted against the code
A problem per mode What problem each of the four modes poses, how the player sees it before any figure, what they change, and how the verdict stays honest. It exists because docs/33 says every stage names what visibly goes wrong and then discharges that word with a goal miss — a goal is a figure, and a figure is not a sight. Its central finding is that no shipped mode satisfies the see-it-first constraint, including Fix a building, which has no stage at all and already throws away a full recording of the failing building on every case open. Measured here rather than assumed: at the shipped day-one configuration the tutorial building's landings are empty about 91 % of the hour, and on 16 of 20 seeds no instant exists at which anybody has been waiting sixty seconds
Vertical slice The one representative ten minutes taken to shipping quality: what is in it, what is deliberately out and why, the quality bar itemised so a reviewer can refuse work against it, and exit criteria that can fail — each naming the instrument that measures it and whether that instrument exists yet. Written against the verified state of the thirteen M2 issues rather than their filed state, which matters: #209 is already fixed, #212 is largely refuted, and #208 is a slot decision rather than a data one
Architecture Three-layer design, core interfaces, DES kernel, determinism strategy
Elevator Reference Elevator classes, speeds, capacities, door and motion timings
Traffic & Statistics Demand profiles, RTT math, replication methodology, CRN, stopping rules
Test Buildings Five reference buildings from low-rise to supertall
Roadmap Phased development plan, with each phase's acceptance verdict and the measurements behind it
Parameterization & Tuning How to tweak every model without recoding, and how to search for an optimum
Handoff Current state, measured facts that bound what you may claim, known-answer tests, open debt
Review findings The whole-system review register, with each finding's disposition
Destination dispatch contract Phase 6's locked interface contract, its measured comparison design, and the open questions that gate implementation
Experience layer contract Phase 9's design: the rules that keep a gamified surface honest, novice/expert modes, a schema-generated dispatcher and traffic editor, and access-zone credentials
TWIN shaft contract Two independently driven cars in one shaft, designed and not built: the shaft model, the speed-dependent separation constraint, the deadlock invariant and the property that catches it, and an acceptance criterion written before the implementation
Design handoff The Claude Design handoff the viewer is built to, the requirements checklist extracted from it, the gap analysis against the shipped viewer, the backend changes the front end required, and every deviation with the constraint that forced it
Building behaviour contract The program that makes the sim read as a building — steps 0, 1 and 2 built, the rest designed: an independent traffic seed, body-mass and group-size distributions you can shape, day-to-day variability, passenger patience and abandonment, lift-lobby crowding, stairs with the up/down asymmetry real people have, and the surface for teaching a learned dispatcher. Every feature opt-in and byte-identical when unused, with the acceptance criteria written before the implementation and the sequencing forced by what can move a published number — including step 2's, which measurement sent back for correction rather than met (§ D203)
Compute offload contract Moving measurement compute off one laptop, and why that is a statistics problem before it is an infrastructure one: common random numbers pair alternatives within one run on one machine, so the unit of distribution is a whole paired comparison and never an arm; and a runner is a pin environment, so a second architecture is a third pin set rather than cheaper cores. Phase A — self-hosted Azure CI runners — is withdrawn and its code removed, on a cost finding: the template billed fixed capacity (≈ $212/month) while the runbook published the ~$5 of a per-job model it did not implement, which is a published number that did not reproduce from the code that produced it. Phase B, the measurement fan-out, is still only designed. Carries the honest ranking of what compute does and does not buy — two of whose four rows died with Phase A — and the criterion that raising the replication budget requires re-measuring the resolution limit rather than inheriting it
Change scope contract What a control is allowed to move, and when. The simulator runs a whole day and plays the recording back, so there is no mid-day change — every change re-rolls the day, which makes the retry the product's most-used verb and, until it was named, one that could bank a scenario on a single Monday. Four scopes named so a fifth is a compile error, the controls under them derived from the state's own keys in both directions, and ten rules S1–S10 — including the one that says a presentation control must reach a sink and must not reach the legs
Static site deployment Hosting the viewer's page on a CDN while the API stays on the Container App, because serve.ts serves the page out of a container running at minReplicas: 0 and a cold first load was measured at 32.2 s against 0.13 s warm — which /api/wake cannot fix, since the page is the thing being waited on. Carries the priced three-way comparison and the trade it honestly is: £0 and same-origin stops being true, versus ≈ £9/month to keep it, versus ≈ £26/month to change nothing but the bill. The three configured values that then have to agree, the two the server refuses to hold apart, and — in the runbook's own voice — a § 9 that separates what was run from what was only reasoned about. Nothing is switched on and no Azure resource has been created by it
Everyday Mode playtest audit #2 The second player-walk, taken after every slice landed: it re-verifies docs/19's fourteen defects as a player (nine fixed, three partial, no regressions, the blocks-play trap gone) and plays the new rules editor, ghost race, Fix-a-building, bench suite and watch flows. Seventeen new findings, none blocks-play, each with a repro and an owning module — plus the session narrative naming the product's best ninety seconds and its worst
Everyday Mode playtest audit The Everyday Mode delivery walked as a player in a real browser — docs/17's successor. Per-flow verdicts on playability, navigation, intuition and information; fourteen ranked defects with reproductions, one blocks-play (the post-reload Resume trap, since fixed with the repro as a browser regression); and the what-would-make-it-fun notes the polish lanes are scoped from
Everyday Mode tree audit The casual-mode design handoff's build plan verified against this tree, slice by slice, after the handoff's own precedence rule fired: the prototype §20 describes is not in this repository, so what it calls inert controls do not exist here, what it calls missing partly exists, and what it says to delete was never built. Carries a verified work-order per slice naming the real seams, the implementation-status register for landed slices, and the disagreement register where the code won
Engineer reimagined contract The Engineer surface rebuilt in Everyday Mode's visual language, with more information rather than less — § D299's test (a change may make Engineer easier to use; it may not make it say less) turned into an instrument: an information-survival ledger naming every figure, qualifier and refusal on all fourteen surfaces with the export that carries it, so a review is a checklist rather than a reading. Carries the token restyle both products share, the added controls and the authoring gap that fails both products (five dispatcher families advertised, two authorable), six engineering challenges scored in verdicts and intervals and never in a grade, and the lane cuts they are built from
Play-through audit The product walked as a player, mode by mode: what each is for, whether it makes sense, what moves at each scope inside it, and what its results page has to say. Carries four modes that do not exist and an argument for each — incidents and maintenance over the serviceEvents scheduler no shipped building calls, a calendar of seasons and holidays at growth.ts's own seam, a fixed-seed daily challenge that would give the leaderboard a competitive axis other than luck, and a commissioning phase over the elevator-spec table's real rise and floor-count gates
Play-test report A tester's session notes, kept as the source the play-test backlog was filed from. Read it as a report rather than a finding: verification has since refuted or re-attributed a large share of what it claims, and the surviving dispositions live in ISSUE_WORKER_LEDGER.md, whose claims that did not survive verification tables are the correction of record
Phase 6c re-measurement handover The pre-registered § D162 protocol, written to be executed cold in its own session: the five conditions and which already hold, the gate itemised, the saturation census that must come first because no budget may be inherited, and what an acceptance would and would not be allowed to say. A third refusal is a permitted outcome — and is now the recorded one (benchmark/lunchTwoWaySelection.ts)

Machine-readable configuration lives in data/, and the design the viewer is built to is vendored in docs/design/.

Running the viewer

npm install && npm run build
npm run dev -w @elevator-sim/viz     # → http://localhost:5174

Deploying it

The viewer and the API ship as one container serving one origin — which is what lets the API's CORS policy stay at same-origin, because there is no cross-origin request to permit. The viewer's web bundle is a separate build from the library one:

npm run build:web -w @elevator-sim/viz     # → packages/viz/dist-web/
docker build -t elevator-sim .

The server needs PostgreSQL (ELEVATOR_SIM_DB), a 32-character signing secret (ELEVATOR_SIM_SECRET, no default — a placeholder is how a development secret reaches production), and, in production only, a real mailer. infra/README.md is the Azure runbook: Container App, PostgreSQL flexible server, and Communication Services for confirmation mail, with the cost model derived from the template's own parameters rather than asserted beside it. Its § 0 says plainly which claims were verified by running them and which are still only reasoned about.

The viewer is served from a CDN and the API from the container, which removes a 32.2 s cold first page load — docs/16-static-site-deployment.md is the runbook, and its § 9 keeps the same split between what was measured and what was argued. Arming it found two defects that reading it had not, both fatal and both invisible until a real token was exchanged (§ D308).

Status

Phases 0–5 and 7–9 are landed and accepted. Phase 6 is partially complete — see the table for what that means, and read Phase 9's row rather than its tick: it is accepted with named gaps, and the gaps are part of the verdict. Four packages (core, experiments, viz, cli), a six-command CLI, and a viewer built to a design handoff, and tsc -b clean.

This section used to publish a suite figure and it no longer does, which is the correction rather than an omission (GitHub issue #230). It said 253 test files and 4,700 tests at 550 s serially, eight lines above a command block saying 226 and 4,148 — two answers to one question inside one section, both stale, and both disagreeing with every other document that has ever measured the suite. The superseded figures are written struck through and outside the shape the guard reads, which is the convention docs/31-support-matrix.md already uses and the reason a corrected document may contain the old number at all. A suite count is a claim about a machine, a commit and a tier rather than about a project, so it belongs where those are recorded and nowhere else: AGENT_STATUS.md carries the measured runs with their host, their tier split and their date, and GAPS.md's header carries the wave-12 figure with the two-OS caveat that makes it readable. packages/experiments/src/validation/documentation.test.ts keeps this section free of a third answer.

Wave 10 rebuilt the viewer against that handoff and moved no phase verdict: no published number was recomputed and no acceptance criterion was touched — the report sheet reads VizSummary, which reads RunSummary, which is the object the CLI and the experiment matrix already read. See WAVE10_PLAN.md.

Phase Status
0 — Foundation ✅ DES kernel, per-source RNG streams, config loading
1 — Physics & model ✅ S-curve motion, doors, load sensor, pure estimateCost()
2 — Traffic & dispatch ✅ Poisson batch arrivals, weighted-cost engine, RTT oracle
3 — Experiment infra ✅ Replication runner, CRN, sequential stopping, paired-t
4 — Visualization ✅ Viewer, building editor, live metrics overlay, playback from a stored seed, 88-scenario UX ledger
5 — Smart dispatch ✅ Twelve cost terms, auction, predictor, benchmark suite
7 — Automated tuning ✅ Search space, three searches, held-out validation, elevator-sim tuneand its one undelivered bullet, the fuzzy traffic-pattern detector with hysteresis driving per-pattern weight sets, now ships and drives a run; measured BETTER on TTD and reported below the resolution limit, because the effect is smaller than the apparatus resolves
CLI list, run, compare, tune, fuzz, watch
6 — Destination dispatch & learned control ⚠️ 6a (disclosure) and 6b (dispatch) accepted against a raised criterion, now measured on the Mixed-Use High-Rise the criterion names: met by the Level-0 arm — and since § D333 by the Level-1 panel too, at the heavy point. The panel had been measured with a defect only it could suffer: #tellThePanel promised every waiter at a landing to one car with no capacity bound. Bounded, ΔTTD at up-peak 4 % (n = 200) is −1.598 [−2.575, −0.621] against eta and −1.642 [−2.620, −0.663] against collective, both BETTER and resolvable at that cell, with AWT and WT95 still WORSE beside them. The two lighter points remain INDISTINGUISHABLE, which is the shape an over-subscription defect predicts. 6c (learned control) is implemented, measured, and NOT ACCEPTED — and the refusal is no longer one operating point: it was swept over eight pre-registered cells and held, refused at all five primary cells under a multiple-comparison correction, with the smallest detectable effect re-measured at each cell rather than inherited from another. Two of those cells clear the correction and were refused anyway, because the effect is a third to a half of what the apparatus can resolve there. The refusal has since held a third time, on the one condition the sweep named as missing: measured under § D162's pre-registered conditions at the mix-varying lunch-two-way point — where the detector's two-way pattern really is the incumbent — the learned arm's ΔTTD contains zero and sits below the cell's own TTD-measured limit, and the flat-mix negative control exposed the remaining advantage as a static weight-vector hybrid, not mix exploitation (benchmark/lunchTwoWaySelection.ts). Double-deck operation is simulated; its verdict became BETTER-EVERYWHERE once a real escalator replaced a lift leg the hardware would never pay for — on two cells at one operating point where the previous answer had four at two, and a better word on a narrower base is not a stronger result. Every sub-phase now has a measurement rather than a deferral, and the phase is still partial because one of them was refused
8 — Testing campaign ✅ All eight tracks landed — fuzzing, oracle across all five buildings, physics, statistics, determinism, scale, adversarial, and the full experiment matrix (8 cells × 12 profiles, Pareto front over AWT / energy / WT95) — and found four real defects, all four now fixed; the deep tier is green at 2 000 cases and no property violation is outstanding, so both halves of the criterion are met
9 — Experience layer ACCEPTED WITH NAMED GAPS — all nine units built, and the two clauses that decide the phase are met by a run rather than by an argument: the honesty property held under search over a corpus of generated strings across every registered surface, with 0 violations and an empty register in both tiers — after finding two, one real and one a check accepting the wrong branch — and the cases, strings, simulations and surfaces are counted in one place only, CLAUDE.md's Phase 9 table, because that figure has been re-measured every wave and a second copy here would be stale within one (this row said 271 985 strings and 23 surfaces for six waves; both had roughly doubled) — and mode parity is derived from the code, not listed by hand. The gaps are named because they are part of the verdict, not beside it: clause 4, every unit names its non-test caller, is satisfied in prose and mechanised by nothing — no dead-code audit reaches packages/viz, so it is the clause to distrust first (closed in wave 12: packages/viz/src/deadCode.test.ts mechanises it, § D192 — verdict unchanged); Escape does not dismiss the drawer (closed in wave 12, § D188); and U6, U7's rider models and Basic's curated three-dimension subset are unbuilt

Try it — six commands, all against the real data/ directory:

npm install && npm run build
npm run sim -- list
npm run sim -- run --building garden-apartments --dispatcher eta --seed 42
npm run sim -- compare --building midtown-office --a eta --b nearest-car --reps 100
npm run sim -- tune --building garden-apartments --params idle.repositionThresholdS --seed 42
npm run sim -- fuzz --cases 8                  # or: --tier deep --cases 2000, the overnight pass
npm run sim -- watch --building garden-apartments --dispatcher eta --speed 10
npm test        # the benchmarks execute real replications, so this is minutes, not seconds

compare prints a paired-t interval on the difference and refuses to rank two arms whose interval contains zero — that is the point of the project, not a nicety. It also refuses to gate on AWT across two passenger models, moving its headline verdict to TTD and naming core's own list of the nine metrics that stop being comparable. run, compare and watch all refuse to print a mean the run's own summary suppresses; watch printed one on both of its render paths until § D111.

What is not done is in the brief, not in this table. Phase 6c is measured and refused, which is a different state from deferred and a better one. Phase 9 — the experience layer designed in docs/10 — has all nine of its units built and, as of 2026-07-30, a status row: a Casual/Engineer split, a schema-generated dispatcher form, per-floor rider queues, a building-mood gauge, an access-credential lens, a seven-stage campaign whose bars are the shipped configuration's own measured scores rather than numbers somebody picked, and a comparison tab that runs a proper replication batch in a worker and shows the interval without naming a winner when the interval contains zero. It carried no status row for as long as it had no criterion, and then for as long as the criterion was unmeasured — the row and the verdict land together or neither does, which is § D163's own rule. Both now exist, and the verdict is accepted with named gaps: what a reader should take from it is not the tick but the four gaps under it, the first of which is that every unit names its non-test caller is a sentence in a document and not a test. TWIN operation — two independently driven cars in one shaft — is designed and not built, in docs/11; it is not double-deck, and the contract says why. A phase's status is now bound to evidence that existsvalidation/phaseStatus.test.ts parses every status and citation out of the roadmap and fails if an accepted phase names a test, study or pin group that does not — but not to evidence that supports it: a phase could still cite a real suite that does not assert its criterion, and the guard cannot tell a raised criterion from a weakened one. See docs/07 § 8.

The browser viewer and building editor live in packages/viz and are dev-served with Vite; packages/core exposes a ./browser subpath so nothing pulls node:fs into a bundle.

The page opens on Everyday Mode, and has since 2026-08-12 (§ D335, § D338). packages/viz/index.html loads everyday/boot.ts, which mounts a rail, a pinned action bar and a four-tile menu over the Engineer surface; the Engineer surface still builds and starts exactly as before, because boot.ts imports dev/main.ts for its side effect. The two worlds co-exist and the rail's footer row is the door between them, with the way back on the Engineer header. The swap is not remembered: a reload lands on the Everyday main menu whichever world the player was in.

This paragraph described the Engineer menu as what a player meets first for six weeks after that stopped being true (GitHub issue #230). It is the class this file's own front matter names — a sentence that outlived the thing that made it true — arriving on the one surface where it decides what a reader thinks the product is. Everything below is unchanged and still accurate; only the claim about what opens moved.

The Engineer menu, accounts and the leaderboard (DECISIONS.md § D214, § D215). Behind the Everyday shell, the Engineer surface opens on a main menu — Campaign, Free Play, Leaderboard, Account, Settings — whose state is a pure reducer with no document in it, and whose Free Play axes are derived from data/ rather than listed. packages/server is the first server this repository has had: accounts with scrypt hashing, email confirmation behind a signed expiring token, opaque session tokens in a table, and a leaderboard whose entries are verified by replaying their seed. That last part is the whole anti-cheat design and there is no other part to it — a client-reported score measures willingness to cheat, and invariant 5 already guarantees the fix, so the server re-runs the submission through the same kernel every study drives and accepts the score only if it reproduces. A board is keyed by a content hash of what it measured, so a data/ change starts a new board rather than silently invalidating an old one, and it ranks on one declared metric with the others beside it — never a composite, for the reason § D106 gives about energy. Its dependencies are node:sqlite, node:crypto and node:http; the repository's runtime dependency count is unchanged. The signing secret comes from the environment and has no default: a server started without one refuses to boot.

Replacing the front end. The layers under the viewer are designed to outlive it: the recording contract, the frame producer, the metric overlays and every layout planner are pure functions of (recording, time) with no DOM anywhere near them, and the two functions that turn a time into a picture clamp their time argument, so scrubbing anywhere — including past the end of a run — cannot throw. packages/viz/src/dev/elementMap.ts is the integration contract for the page itself: every element the viewer resolves, in one list, checked against the shipped HTML in both directions. The second direction is the useful one — it names the 34 ids in the page the viewer never looks up, which is the answer to what can new markup safely drop? A page missing elements now gets one message naming all of them rather than dying on the first. One honest limitation, recorded rather than glossed: every element is still required, because the viewer dereferences all of them unconditionally, so "this build has no campaign tab" is not yet expressible as a disabled surface.

See the Roadmap for per-phase acceptance verdicts and the measurements behind them, and the Handoff brief for current state and open debt.

License

MIT — see LICENSE.

About

Configurable elevator traffic simulator for designing and benchmarking smart dispatch algorithms — realistic physics, multi-zone buildings, and statistically valid comparison.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages