Catering management · build status

Mise

Recipes, allergens, costing, production and the service gate. The ledger below is the authority; this page is generated from it.

generated 2026-09-14 06:45 UTC tree 052788e 79 slices from docs/BUILD-LEDGER.md

Overall

96%complete
76 complete3 in progress 79 total

Phases

Phase A — the event production spine

M1 – M16 · 16 slices

16 complete

Phase A′ — settlement and compliance

M17 – M18 · 2 slices

2 complete

Phase B — online ordering and drop-off (market 3)

M19 – M22 · 4 slices

4 complete

Phase C — the service floor (market 5)

M23 – M28 · 6 slices

6 complete

Phase D — analytics, waste, carbon (markets 4 and 6)

M29 – M34 · 6 slices

6 complete

Cross-product couplings and the site

X1 – G34 · 45 slices

42 complete · 3 in progress

Every module

SliceNameStatusThe ledger’s note
Phase A — the event production spine
M1FoundationcompleteSchema, mise_rw role, migration tooling, health fingerprint, token symlinks, app shell
M2Company, identity, auditcompletePersona, polymorphic auth, sessions, cloned roles, append-only audit. Isolation proved for read AND write
M3Kitchens and capabilitycompleteCounted named resources, capacity windows, storage bands. UI: list + create + add resource
M4Products and supplierscompleteUnit conversion, dated prices, declared allergens with source, nutrition, carbon
M5Recipes and the resolvercompleteNested sub-recipes, per-step yield, cycle guard, recipe_ingredients()
M6Costing functionscompleteProved against an independent Python oracle — 37 assertions, every figure agrees
M7Dishes and menuscompleteDish↔recipe, equipment rules per (dish × style), menu structure
M8★The Allergen ChaincompleteDerived through nesting, ingredient_declaration() as marked-up spans, both labelling regimes, review dates
M9Clients, functions, service momentscompleteEnquiries, tastings, options with expiry, service moments, guest requirements
M14★The Service GatecompleteSix legs, all real functions. No stub remains. not_assessed now means "there is nothing to assess" rather than "not built". The sixth is dietary (D-034): a certify requirement inside its supplier lead time blocks the price and names itself
M10★The Kitchen PlaybookcompleteFacts with provenance (who, when, which job, how) and expiry derived from a volatility class — 5 years structural, 6 months personnel. Blocking facts refuse the gate. Discrepancies keep both values. A correction is a FUNCTION that lists every gate verdict which consumed the old value
M12BEO and versionscompleteQuote versions are immutable once issued and carry the gate verdict. Closed 2026-08-23. The diff and the function sheet existed (D-065); the gap was distribution, now built: mise.beo_distribution (0071) records who holds which issued version, beo_distribution_state() returns current/stale, and an EMPTY result means nobody was sent it — a third state the screen renders as a warning, not a tick. Erasable by design (Article 17), with the erase as a visible control
M13Production planningcompleteBackward scheduling from serves_at, prep list ≠ pack list, batches with lot traceability and trace_lot(), cold-chain legs with a reason per leg. ★ The Pass — the full-bleed gloved-hand surface. 16 assertions
M15Purchasing and stockcompletePurchase orders, goods receipt with lot and expiry, an append-only signed movement ledger with no stored balance, stock counts that post adjustments, purchase_requirement() from the resolver, and theoretical-vs-actual with its causes separated. 17 assertions
M16Crew and labourcompleteOne labour engine for quote and timesheet. Double-booking refused by a Postgres exclusion constraint, proved by raw insert bypassing the API. 22 assertions
M11★The Costing SpinecompleteRate cards, quote versions with per-line VAT in basis points, contract with an enforced final-numbers deadline, settlement, variance attributed to causes that sum exactly. Screens for both. 16 assertions
Phase A′ — settlement and compliance
M17SettlementcompleteBuilt with M11
M18HACCPcompleteTask-shaped, not document-shaped. A missing record counts as a failure, a CCP on a recipe step generates its own check, and a batch is not closed until its checks are. 21 assertions
Phase B — online ordering and drop-off (market 3)
M19Ordering channels and cataloguescompleteorder_channel, catalogue, catalogue_item. The channel's service_style is the load-bearing column: mise.labelling_regime() turns it into a regime, and a channel demanding a label refuses a dish whose label cannot be derived — in the database, not in a screen. Screen 26-channels
M20Customer and group orders, individually labelled itemscompleteThe individually labelled item is the slice's reason to exist. Twelve people, one delivery, each box labelled with a name: order_line_item.recipient_label. label_print is append-only and carries no recipient name and no dietary note. Screens 27-orders, 28-order
M21Recurring orderscompleteStanding orders. Generation is idempotent by a PARTIAL unique index on (schedule_id, occurrence_on) — the generator never checks first, because a check is a race and this runs from a cron. Looks BACKWARD as well as forward, so nothing missed while it was down is silently skipped. 24 assertions
M22Delivery runs, stops, route sheetscompleteOne order can be on at most one run, by index. A run cannot be completed over an undelivered stop, by trigger, and the refusal NAMES the stop. route_sheet() READS M20's dispatch guard rather than restating it. 30 assertions
Phase C — the service floor (market 5)
M23Covers, tables, seatscompleteTables are FURNITURE — no guest identity on them, because a second home for Article 9 data is a second erasure path to get wrong. seating_summary() compares seats laid against covers charged, and no_plan is not short
M24Service tickets, station routingcompleteRoutes onto the station vocabulary that already existed. station_code has NO foreign key on either side — measured — so ticket_routing_gaps() names three different gaps and the board keeps an unrouted item VISIBLE rather than dropping it
M25Course firing and pacingcompleteThe same engine as M13: recipe_cook_minutes() extracted and plan_moment() rewritten to call it. Nothing is paced until the owner sets a course gap and a plate rate, and the missing one is NAMED
M26The Pass (KDS surface)completeA second full-bleed surface, keyed on a service moment rather than a plan. Its first screenshot exposed two real defects — see below
M27TendercompleteHOW a sale was settled, not a payment processor. Append-only and signed like the stock ledger: a refund is a negative row, not an edit. Four states, because nothing_taken is not short. There is nowhere to put a card number and the test asserts it
M28Bar tabs, consumption, corkagecompleteONE tender ledger — tender gained a tab_id rather than M28 growing its own. A closed tab cannot accrue, by trigger, and the refusal names the tab. No state column: closed_at is null IS the state
Phase D — analytics, waste, carbon (markets 4 and 6)
M29Waste capture attributed to recipe and stagecomplete + E2waste_event, attributed by stage. Screen 30-waste. This row read "Phase D — not started, schema exists, no screens" until 2026-08-23, which was wrong on both counts
M30Carbon per dish, menu and functioncompleteOne dataset or none — a product carrying factors from two sources resolves to NO factor rather than a blended one. Every figure renders its attribution beside it
M31Menu engineeringcompleteScreen 32-menu-engineering
M32Forecasting from history and leftoverscompleteRefuses below three services and will not mix units. A forecast from two services is a guess wearing a number
M33Group analyticscompleteBuilt 2026-08-23. mise.group_performance(from,to) rolls every function up by client and by kitchen. Its whole design is three sums it REFUSES: contracted is never added to occurred, an unsettled function is NULL revenue rather than zero (with unsettled_finished beside it), and two currencies are two rows. Screen 38-group-performance
M34Sustainability pack for tenderscompleteBuilt 2026-08-23. mise.sustainability_pack(from,to) publishes a carbon figure ONLY when a dataset is named, a credit line exists, something was sourced and every recipe's units match its yield; otherwise carbon_kg is NULL and refusals says why. Coverage is reported either way. It invents no minimum-coverage threshold — that judgement is the caterer's. Screen 39-sustainability
Cross-product couplings and the site
X1event.clinic readerunblocked and liveReader, snapshot model, sync check and exception lifecycle all built and tested. Q-011 is answered: event.clinic built app.event_participant_summary() and the supplier-principal mechanism, and Mise is identified by the database role it connects as (session_user), impersonating no tenant. Measured this session: the engaged event returns rows as mise_rw, an unengaged one returns none. D-017 and D-019 held throughout
X2EstatecompleteGET /api/venues/:id joins the venue, its spaces, Estate's booking diary and Mise's own kitchen record. Reads Estate defensively and returns estateReadable: false rather than an empty array when its schema differs
X3Procurevent biddingpartialBid requests, requirements, responses and the submission gate all built. A bid cannot be submitted without a Service Gate evaluation — a database constraint, not a convention. No transport to Procurevent: Q-005
X4Verbunk crewpartialD-088 made agency_worker_ref writable: the route accepts it, refuses an agency member with no agency named, and refuses agency details on a non-agency kind. The seam is usable from Mise's side; what is absent is transport, exactly as X3 and X5. Q-007
X5VELA weddingspartialThe inbound receiver stores verbatim then processes, and turns a guest list into seat-level requirements. It refuses to guess which sitting a requirement belongs to. No transport: Q-007
X6Rigfold dock contentioncompleteBuilt 2026-08-24. Rigfold's 0080 answers the handover: av.dock_windows(venue_id), a definer function returning four facts, gated on an empty av.catering_reader. Mise reads it on the venue endpoint and distinguishes a closed door from a quiet dock
G1Article 17 erasurecompleteBuilt 2026-08-30. guest_requirement was erasable by design and nothing exercised it. Two calls, never one: the preview is the only place a person learns what cannot be erased. The meal answering a requirement is KEPT — unlinked and redacted — because destroying the record that a dietary need was answered is over-erasure dressed as caution. Append-only rows containing the name are named, and the result says so in words: "erased" alone would be read as "gone"
E1Central production unitcompleteBuilt 2026-08-29 on the owner's ruling, and built to be OPTIONAL — company_runs_satellites() lets a screen ask, and a caterer with one kitchen is told so rather than shown an empty table. A transfer is two movements with one id summing to zero, and the lot code travels, so a recall follows a batch across the van
E2Waste captured where it happenscompleteBuilt 2026-08-25. The third full-bleed surface. Records what a person typed and refuses to weigh anything — a figure from a photograph is a guess wearing a measurement's clothes
E3Supplier price filescompleteBuilt 2026-08-29. Holds a supplier's list with its date and reports where the held price disagrees. Never updates a price: prices are held with their date and never re-derived. Matching is on the supplier's code, exactly — a near-miss prices the wrong ingredient into a tender
E4NutritioncompleteBuilt 2026-08-29. Computed and displayed, never claimed — Annex XIII wording belongs to whoever signs the label. Stricter than carbon about publishing: a declaration at partial coverage has the shape of a complete statement
G2VAT with a stated basiscompleteBuilt 2026-08-30, closing Q-003 (D-110). A rate travels with the rule that produced it, or Mise reports that none did. Building it found that nobody was choosing a rate at all: raise_purchase_orders() never named vat_rate_bp, so 27% went out on every order from the column default with nothing on screen saying so. The basis is frozen onto the line, not joined — a rate is held with its date and never re-derived, like a price. vat_unruled() names what no rule reaches. Completed the same day (D-119): all seven sites wired — it was seven, not six — through one ranking with two doors, and the sales side resolves from mise.charge_kind, a vocabulary that already existed. D-120: the fallback is no longer 0
G3Publishing, and the caterer decidescompleteBuilt 2026-08-30 (D-128, D-135). Menus and kitchen capacity may reach event.clinic; allergens, prices, costs, suppliers, guests and kitchen notes never do. ★ Off until chosen, with three states — never asked, considered and declined, published — and decided_by/decided_at making the consent checkable rather than asserted. Mise exposes and event.clinic pulls, so Mise never writes to app survives. The caterer's preview is assembled by the function that serves the feed, so it IS the payload rather than a description of it
G4What Mise cannot answer forcompleteBuilt 2026-08-30 (D-137). Eleven gap functions existed and nothing asked them together — right for the person reading a figure, useless for the person signing one. Composes them; no new domain logic. ★ A GAP is separated from a DEFECT, severity is quoted from recorded decisions rather than invented, and n = 0 rows are omitted so a clean area is not a finding
G5★A definer function in mise is not publiccompleteBuilt 2026-08-30 (D-143). D-113 was found by asking what mise_rw reaches through app's SECURITY DEFINER functions. Nothing had ever asked the mirror question, and the answer was 7 of 9 definer functions in mise executable by PUBLIC — auth_lookup returning a password hash, session_create minting a session for any user account, provision_company creating a login — reachable by app_rw, which holds no privilege at all on the four identity tables. ★ The two already closed came from 0094, written the same day: the discipline was in the newest migration and absent from the oldest, and no check looked. Dev was worse than local — six roles hold usage on schema mise there, including estate_rw. 0097 closes all nine with a catalogue loop and three in-transaction assertions; privilege-audit.sh §5 keeps them closed, sabotage-proved by name. Applied local and dev; production unreachable
G6★Nothing leaves the building silentlycompleteBuilt 2026-08-30 (D-144). Password reset was zero files and email sending was none, so a caterer who forgot a password was locked out permanently. mise.outbox is the RECORD that a message had to leave, not a queue: three check constraints make the states honest, and the one that matters refuses a failure with no stated reason — what an empty catch block writes. A sent message drops its body (a BEO carries Article 9 data; an outbox is not an archive). The request is uniform (202 either way, no enumeration); the consume names its refusal, revokes every session, and does not sign anyone in. Provider is the Cloudflare Email Sending binding, chosen because it needs no API key — ★ nothing can send yet: no sending domain is onboarded, and that is recorded in the outbox rather than hidden. readiness() names the gap; /system shows the technical reason; the caterer sees plain language
G7★The way in, the legal drafts, and CIcompleteBuilt 2026-08-30 (D-145, D-146, D-147). Three of the handout's named gaps. The marketing site had no link to the app on any of its five pages; there is a Sign in in every navigation now and the hero's primary action is *Start a company*. Privacy notice and terms are drafted and NOT published — that is an owner action — with the half an assistant can do measured: no append-only table has a column FOR a name, but 13 of the 18 carry free text where one can be typed and can never be removed, enumerated. Five owner decisions are Q-013. CI runs on every push and is green: source and schema need no secret, full waits on a token for the sibling design-system repo and is skipped *with the reason stated*. The first CI run went red on a constraint db/_lib.sh had already written down — one PGPASSWORD, two URLs
G8★Does the data come back?completeBuilt 2026-08-30 (D-148, D-149). Two of the operating gaps. The deploy proves itself: /api/health reported a hardcoded commit: "dev" for every build ever deployed, so it could not tell a current Worker from a stale one — it carries the real short SHA now, with -dirty when the tree is, and deploy-dev.sh probes the feed and a deliberately invalid control after every deploy. Errors are retained: the Worker already logged them; observability was what was missing. ★ And the restore rehearsal found on its first run that pg_dump --schema=mise is unrestorable — GiST over uuid needs btree_gist, which lives in public. It compares all 130 row counts and a computed costing, because a restore that restores nothing raises no error
G9★Throttled, not locked outcompleteBuilt 2026-08-31 (D-150). G6 had shipped a password-reset route anybody can POST to. No account lockout — it would let anybody who knows a caterer's email lock them out of their own kitchen; NIST SP 800-63B agrees. Cloudflare's rate-limiting binding, no account and no key. Two limiters because they protect different victims: IP at 20/60s against password grinding, email address at 3/60s against mail-bombing one person, and the reset limit answers with the *same* sentence as the IP limit so it cannot confirm an address exists. Fails open when unbound, said out loud in three places. ★ An eventually-consistent limiter cannot be probed sequentially: six sequential requests were all accepted while thirty concurrent gave 12/18 — the first probe was a false negative about working code
G10★A label attached to nothingcompleteBuilt 2026-08-31 (D-152). The accessibility pass, which was *still untried*. ★ Field puts the control in a SIBLING slot, so a <Field label="…"> with no htmlFor renders a label attached to nothing while the source reads as perfectly labelled — 13 of 202, and VatRules had 6 of 6. 67 more controls had no accessible name; 43 took one from a placeholder and 24 were named by hand. ★ The mechanical pass turned placeholder="0" into aria-label="0" — a name worse than none, which no static rule could catch and only reading the diff did. verify-accessibility.sh proves itself on a wrong fixture before scanning, and is sabotage-proved both ways. Contrast and focus order are untried: that half needs a browser, and the machine was at load 284
G11★The screens check ran, and the suite stopped eating its own reasonscompleteBuilt 2026-08-31 (D-154, D-155). The one part of the suite that had never run. It runs: 66 passed, 0 failed, 0 blocked, 0 skipped, exit 0 — the first complete verify-all.sh local --with-ui in the project's life, all 53 screens included. Getting there found a defect in the summariser itself: run() printed its reason from the FAILED branch only, so the first block said nothing was measured and no more, and what screenshots.mjs had explained is now permanently unknowable because it was discarded unread. ★ The fix was half-broken and the diff read correctly: the anchor required leading whitespace while mise_detector_broken prints at column 0, so DETECTOR BROKEN — *this check's pass is worthless* — still printed nothing; an exit-4 stub caught what reading did not. Every block now carries all three load averages, at all five of screenshots.mjs's own sites too, because a standalone run never reaches the summariser. The 300 s budget was deliberately NOT raised (D-155). New guard verify-suite-reports-why.sh: four stubs, a negative control, and exit 4 if it cannot find run()
G12★What only a browser can seecompleteBuilt 2026-08-31 (D-156, D-157, D-158). The browser half of the accessibility pass, recorded as untried since G10. 48 screens, both themes, 4233 text runs, 1103 real Tab stops. It found what a source scan structurally cannot: .mise-stage carried opacity: 0.55, and a container opacity drags its descendants' text with it — correct tokens measured 3.42:1 and 2.32:1. verify-tokens.sh passed, the source a11y scan passed, and the CSS said nothing. ★ Its own first run reported 62 failures and 60 were the check's fault: opacity: 0 tooltips read as 1:1 (invisible is not low-contrast), and el.focus() reported every control as having no focus ring (:focus-visible is a heuristic about how focus ARRIVED). Real Tab presses fixed it and gave the true focus order for free. Three upstream rules fail in dark and are NOT fixed here — exempted by selector with a counted ceiling and handed to event.clinic as U-009. The screen list moved to scripts/screens.mjs so the two browser checks can never cover different screens
G13★The draft must not stop saying it is a draftcompleteBuilt 2026-08-31 (D-159). Q-013's five owner decisions are marked inline in the legal wording, at the point in each sentence where the answer goes. verify-legal-draft.sh asserts each one is still marked in the body — checking the section 0 table would pass on a document whose every blank had been filled in with an invention. The roster is read from the file, so a reshaped table exits 4 rather than passing on an empty set. It publishes nothing; publishing stays an owner action. ★ Its own DRAFT sabotage returned 0: head -5 | grep DRAFT passed after the H1 was changed, because line 3 independently says *THIS IS A DRAFT*. A window is not a line — split into two named assertions, both re-sabotaged
G14★Two checks vanished, and nohup was killing the servercompleteBuilt 2026-08-31 (D-160, D-161). Two findings, both found by noticing something did not add up. (1) The Screens section had three checks and four branches, each naming only one; a servers-down run reported 66 passed … 1 skipped for a suite of 69, and the two a11y checks were absent rather than skipped. Reading the branches could not find it — arithmetic did. Now one UI_CHECKS list, proved both ways against dead ports. (2) ★ The API died eight times; the load diagnosis was written into the ledger and was wrong. It died at load 22 and while idle, and the decisive evidence was an absence — no error and no Logs were written to line, so it was terminated, not crashed. nohup hands /dev/null on stdin; wrangler dev watches stdin and exits on EOF. Holding stdin open with ( sleep 86400 | npm run dev & ) it survived 240 s idle and a full 346 s battery
G15★The narrow layout, and a green from a check that had stopped measuringcompleteBuilt 2026-09-02 (D-162, D-163, D-164, D-165, D-166). The handout's first item was mobile-width accessibility; the second was the undiagnosed ProxyController crash; they turned out to be the same session's story. (1) a11y-browser.mjs takes --mobile and the suite runs four corners — two themes x two widths — plus a sixth rule, 1.4.10 Reflow, asked of the DOCUMENT so that deliberate horizontal scrollers are not permanently red. Each mobile run proves the narrow layout actually engaged before trusting itself, geometrically and with exit 4, because widening MOBILE past the 900px breakpoint would otherwise turn every mobile run into a second desktop run still printing *(mobile) ... clean*. 48 screens, both themes, 390x900: clean. (2) ★ And the first mobile run was a lie. The API died 22 s in and the run reported *48 screens scanned ... clean, exit 0* — a dead API does not break a browser check, it just makes every screen render an empty state, and an empty state has flawless contrast. Reading the output could not find it; arithmetic did — 4233 text runs at desktop against 1740 at mobile. stillOurs() now asks after every screen in both harnesses, for the FINGERPRINT rather than a status code, because 8801 was answered by procurevent in the minute after the death. (3) scripts/dev-api.sh supervises the server: refuses a port a sibling holds, holds stdin open, watches health rather than liveness — wrangler does not exit when its runtime dies, it hangs — and restarts. A hung server was back in 17 seconds against three to five minutes. (4) The crash is localised, not diagnosed: wrangler's own source shows the lost connection is the INTERNAL leg, and client aborts and socket closes were falsified against controls. The idle-timeout hypothesis was contaminated by an unrelated pkill and is discarded rather than written up. (5) And docs/README.md — the file CLAUDE.md sends every session to first — was pointing at a handout four revisions old, with nothing red anywhere, because nothing was broken: a session obeying it would just have worked from superseded state. verify-handout-pointer.sh compares it with what is on disk (D-166)
G16★What no test can reach — and the tenancy leak it foundcompleteBuilt 2026-09-03 (D-170, D-171). Two instruments were measured and rejected before one was built: pg_stat_user_functions is blind because 123 of 146 mise functions are language sql and inline away — the costing suite exercised recipe_ingredients(), THE resolver, and the view recorded zero calls while counting convert_qty 471 times; and grepping the test files is blind to every function a trigger, policy, view, CHECK, default or index reaches. So reachability runs over the call graph plus the catalogue, over-approximating on purpose so the number is a floor. ★ Its first answer was wrong and its own mutation probe caught it: 15 functions filed as *ships, untested* included session_resolve, and breaking it reddened the screens check — the browser half drives the real API, so api/src is a test root. 19 → 4. ★ Then it cited its own comment and read 4 → 0; comments are stripped now, with a control. ★ AND THE REMAINING FOUR LED TO A LIVE TENANCY LEAK (D-170): v_playbook_fact and v_stock_on_hand ran as their owner, who bypasses RLS, and returned 4 and 12 rows with no company context where the tables returned 0. Fixed by 0099 — and v_catering_for_venue deliberately not fixed, because it is D-128's window for app_rw. Ships with mutation-probe.sh, whose mutation parses no bodies: a plpgsql function that raises satisfies any return type
G17★A run that leaves a record, and the record carries its scopecompleteBuilt 2026-09-05 (D-175). battery-record.sh appends one line per run and answers --last-pass. ★ The scope is recorded, not just the outcome: verify-all.sh local silently skips the five browser checks when no server is up and prints a summary of the same shape, and this week produced two greens that meant nothing — a check reporting 48 screens clean against a dead API (D-163), and the same check reporting 48 screens clean having measured zero of everything (D-174). A history recording only *pass* would have kept both as successes. An unparseable summary records ?, never 0; a dirty tree marks the commit *; the load is recorded because the machine has been the confounder twice. No schedule is installed — twelve minutes of Chromium on a Mac with five wrangler servers is not started behind somebody's back; the cron line is printed for the owner
G18★The help surface, leading with what Mise refuses to answercompleteBuilt 2026-09-05 (D-176). A ? in every top bar opens a Drawer about the screen you are on; /help is the index. ★ The refusals are the content: five families — not assessed, unpriced, unreviewed allergens, unsourced, no rule — each saying what was withheld, why, and what resolves it, with the wording derived by grepping the screens rather than invented. The index groups by refusal first, built from the data. verify-help-coverage.sh asserts all 33 surfaces have a topic and every topic a surface, sabotage-proved three ways. ★ The dark ceiling caught the first version at 79 against 22 allowed — it used .link, one of the three upstream rules known to fail AA. Rebuilt on .nav-item and .chip. ★ Then it caught a subtler thing: a drawer shot on the dashboard read 23 with no new markup, because that screen's one .link was counted twice. The ceiling counts runs and is coupled to coverage; the shot moved rather than the ceiling
G19★The user manual, generated from the same source as the helpcompleteBuilt 2026-09-05 (D-177). docs/USER-MANUAL.md, generated by build-manual.mjs from help.ts so the manual, the drawer and the navigation cannot disagree; only the prose is hand-written and --check fails a stale copy. ★ The first generator regex-parsed the TypeScript and was confidently wrong twice — it credited two screens with refusals they do not have, and truncated a fix mid-sentence — while its own guard passed, because it counted topics and both counts were right. A count is not a check on content. It imports the module now. No sentence claims compliance: allergens are derived and an unreviewed product says so, rates carry the instrument they came from, food-safety entries are records and Mise inspects nothing
G20★Input, at the trust boundarycompleteBuilt 2026-09-05 (D-178 to D-181). Two coercions at the boundary turn rubbish into a plausible value, and neither announces itself. ★ Number(x) is NaN for {}, for "tomorrow" and for an absent key — and Postgres ACCEPTS NaN, because it orders NaN above every other numeric value, so check (quantity > 0) passes it and sum() spreads it. Census: 45 numeric columns, 41 accepted NaN, the four exceptions accidental. Measured end to end: POST /api/labour-rules {"maxShiftHours":"not a number"} → 201 Created, and shift_breach() asks elapsed > max_shift_hours, which is false for every number — a rule that silently stopped enforcing the maximum shift. ★ JS and Postgres order NaN oppositely, which is why the sites that already had if (!(x > 0)) throw were safe and reading the SQL alone would not have found it. ★ And new Date() invents a date: the driver resolves a ::date parameter through it, reading "5" as 2001-04-30 and rolling 2026-02-30 into March — on batch_component.expiry, an append-only table. requiredString(x, 'Start', 40) had been standing in for a calendar at seven sites. Closed by 0100 (a finite bound on all 41), finiteNumber() / isoDate() / isoTimestamp() / isoTime() in kit.ts, 47 call sites converted, every date-ish query parameter validated at the router, and SQLSTATE 22P02 mapped to a 400 with a sentence in one place instead of a bare 500 at a hundred. Gated by verify-input-boundary.sh (12 controls, empty exemption list) and db/test/finite-numbers-test.sh (census from the catalogue, so a 46th column cannot slip in). ★ The gate needed three of its own bugs fixed first, each caught by a control rather than by reading it
G21★Output, cross-checked against what the screens readcompleteBuilt 2026-09-06 (D-182, D-183). verify-output-contract.mjs compares what a route RETURNS with what its screen READS, both directions, parsing the screens with TypeScript's own compiler and comparing against live responses a real browser asked for. ★ A TypeScript interface on a response is a claim, not a check: api.get<T> casts and validates nothing, so a renamed field is undefined, React renders that as nothing at all, and every existing check passes because the page drew. First run: 49 screens, 73 responses, 102 comparisons, 11 fields no consumer declared. ★ mise.room_fit() was the worst — shipped in 0039, with its own battery check, returned under a comment calling it *"the one a caterer must see before a price goes out"*, and rendered by no screen; FunctionDetail.tsx already carried a note about losing course_gap_minutes the same way. ★ The BEO's can was two hardcoded trues the screen also ignored, so a person without function.manage got an enabled Issue button that 403'd. ★ A venue's per-field provenance was invisible, so a model-inferred capacity of 900 read as a checked fact — *show the gap beside the figure*, never applied to the directory. Four fixed, six left with their reason in docs/FINDING-G21-unread-response-fields-2026-09-06.md, ceiling six. ★ The check's own two bugs were found by measuring: awaiting res.json() in the response handler stalled page.goto at 30 s on a route that answers in 0.3 s (load was 181 — the comfortable wrong answer), and 49 full page loads on a hash-routed SPA are 49 chances to stall
G22★The data itselfcompleteBuilt 2026-09-06 (D-184, D-185). isolation-test.sh proves company isolation deeply and on one table; a policy reverted on mise.quote would not have disturbed a line of it. policy-shape-test.sh asks the same question of all 130: the shape from pg_policies — RLS enabled and forced, every write policy carrying its own WITH CHECK, every predicate that exists constraining company, zero RESTRICTIVE — and the behaviour as mise_rw, no row belonging to a company visible without a company context. ★ THE COVERAGE FIGURE RECORDED HERE — 18 measured, 100 empty — WAS WRONG, and G27 (D-195) says why: the company was chosen with an unordered limit 1 and it picked the sparsest of six. It is 103 measured, 15 genuinely empty. Kept as written because the number was published to the owner as O-3. ★ Its rule was wrong twice first, both times a coalesce over a predicate that is absent by definition: round one reported every SELECT policy, round two every INSERT policy. ★ using (true) is not automatically a hole — eight policies have it, all on reference tables with no company_id, and mise.role returns its six shared templates by design, so the rule counts rows belonging to a company rather than rows. ★ AND THE ERASURE-SAFETY LIST SAID FOURTEEN WHILE THE DATABASE SAID EIGHTEEN. beo_version, corrective_action, settlement and variance_line each carried an append-only trigger and were named nowhere in CLAUDE.md — the paragraph somebody answering an Article 17 request reads. The guard that existed asked one direction and proved the wrong thing: to_regclass(...) is not null says the table exists, not that it is append-only. append-only-census-test.sh reads the list out of CLAUDE.md rather than keeping a second copy, asks both directions, requires each trigger enabled and firing on UPDATE and DELETE, and proves the refusal as the owner with a control. Every rule forced red
G23★Manual researchcompleteDone 2026-09-06 (D-187, D-188). In docs/FINDING-G23-upstream-and-regulatory-2026-09-06.md, with every source named. ★ A caterer ticks a box and the cold-chain gate said "produced in a CERTIFIED kitchen" — Mise holds no certificate, issuer, issue date or expiry, and that verdict is feasible. 0101 changes the wording only: *recorded as* certified, and Mise does not verify it. The verdict values stay, because a caterer's own declaration is a legitimate input; the design question — who records it, what evidence, what happens when it lapses — is O-1 for the owner in docs/13-owner-items-2026-09-06.md. ★ The three regulatory claims HOLD: VAT rates carry real instruments and the column is headed *Recorded*, not *Effective from*; the one screen rendering an allergen set shows the gap beside it. A sweep for the word unreviewed reported seven screens and all seven were false — grepping for a word is not measuring a property, and shipping it would have made the third detector this repository correctly refused. ★ Upstream: 21 of 246 granted in app, 1 of 78 in estate, 1 of 147 in av — and the narrow ones are right, because Estate answered Mise's request for estate.booking with a narrower grant (that table carries client names and contract values). That began as a finding and the source corrected it. The one real discrepancy is a name: Truss became Rigfold on 2026-08-03 and Mise's docs missed it for a month
G24★The procedures, each one rehearsedcompleteDone 2026-09-06 (D-189, D-190, D-191). docs/14-procedures-2026-09-06.md. Written is not the bar — every procedure was CARRIED OUT on local, and two of them failed the first time. ★ The Article 17 preview searched 5 of about 30 append-only text columns, missing corrective_action.what_happened — a free-text account of a food-safety incident, the likeliest place a guest is named. It could report "0 notes contain the name" while the name sat somewhere nobody can ever delete. 0102 discovers the columns from the catalogue and reports WHERE. ★ The restore procedure produces a database that silently accepts double-booking a crew member: psql exits 0, 130 tables arrive, and the one EXCLUDE constraint is gone because btree_gist lives in public. The corrected procedure is verified by counting FIVE object classes against the source — three of the five were right in the broken restore. ★ Onboarding cannot be undone: the first audit row locks the company in place for ever; deactivation is the reverse, proved both ways. ★ There is no period close in Mise — settlement is per function, so the document says so rather than inventing one. Rotating credentials is written and explicitly NOT rehearsed, because an assistant must not handle secrets
G25★The one honest amber, closed by measurementcompleteBuilt 2026-09-07 (D-193). The handout's first item. Every accessibility run ended *"13 text run(s) NOT ASSESSED: drawn over a background image or gradient"* — both widths, both themes, every run, for a week. The refusal to composite an unknown was right; stopping there was not. a11y-browser.mjs now makes the text of exactly those runs transparent — color only, so no box moves — photographs the screen, and reads the pixels the browser actually painted, with the worst one deciding the ratio because 1.4.3 is a floor and not an average. The browser decodes its own PNG through a canvas: ten lines, no dependency. ★ The count was never 13 components. It was ONE CONTROL — the 13 <option> elements of select#c-skills, because select.input carries the chevron as a background-image and a <select multiple> is the one select whose options are laid out at all. A number with no names could not have said that, so the line prints names now. ★ THE FIRST REAL RUN REPORTED THREE FAILURES AT 2.64:1 AND ALL THREE WERE FALSE. A size=8 list box holding 13 options lays out all 13; rows 10 to 13 have honest boxes 4px to 53px below the select's own bottom edge, and elementsFromPoint at the centre of *"Head waiter"* returns P.input-hint. The pass was reading the hint paragraph underneath the control. Every box is clipped to the padding box of every clipping ancestor now, and what is clipped out is scrolled into view and photographed again. ★ Two browser facts a probe gave and reading CSS could not: a Range inside an <option> exposes no client rects, and .drawer is right: -480px when closed — fully in the DOM, fully off the viewport — hence a11yClick, honoured by this check and not by the screenshot, because a second shot double-counts markup against the U-009 ceilings. All 13 assessed, 0 NOT ASSESSED, in all four corners. 15 planted faults, four sabotages, each caught — and the element-box fallback had no proof until a sabotage showed it silently returning 13 runs to amber
G26★Reachability, asked of the METHOD — on the third attemptcompleteBuilt 2026-09-07 (D-194). The handout's second item. verify-reachable.sh greps route PATHS, so POST /functions/:id/gate runs the gate, GET /functions/:id/gate is its history called by nothing, and the sweep passes — correctly by its own definition. The 2026-08-30 attempt reported 71 pairs and died on its first spot-check, because api.post(/quotes/${id}/${what}) builds its last segment at run time. ★ The fix is one rule: a dynamic segment is a WILDCARD, never a literal, so the sweep may miss an unreachable route and may not invent one — a false negative costs coverage a person can still find, a false positive costs the credibility of the whole suite. That exact call is one of five controls: if POST /quotes/:id/issue reads as unreached the sweep refuses to run. Both sides parsed with TypeScript's own compiler; every api.* path asserted to be a literal on every run at exit 4. ★ 237 routes, 235 api calls, 4 exempt, 30 with no control — six verified by hand before the list was written, all six confirmed. Three are not backlog: nothing can record product.allergen_reviewed_on, the Compliance screen reads the temperature log and cannot write to it, and POST /labour-rules — the route D-178 wrote NaN through — is reachable from no screen. docs/FINDING-F001-built-and-unreachable-2026-09-07.md. ★ Inventoried, not failed and not dropped: failing on thirty pre-existing gaps makes a permanently red suite, which is how a check stops being run. The list prints in full every run, and the run fails on a route on neither list or on a listed entry becoming reached — good news that still fails, so the list can only shrink deliberately
G27★The RLS proof measured 18 tables and could have measured 103completeBuilt 2026-09-07 (D-195). The handout's third item was *"extend the demo seed — 100 tenant tables have no rows"*, carried from O-3. Both were wrong, and the same measurement disproves them. policy-shape-test.sh chose the company to measure with select company_id from mise.person ... limit 1 — the unordered limit 1 CLAUDE.md already has a section about — and here it decided how much was measured, not a pass or a failure. The demo holds six companies; that query returns one with rows in 18 tenant tables, while *Tarragon & Co* has rows in 100. Measured as the owner, only 15 of the 118 hold no tenant row at all. ★ A wrong measurement is worse than none, because it gets planned around: it became an owner item and a handout task worth weeks of fixture work for tables that already have rows. ★ The fix is a semantic, not a seeder. The assertion is per table, so the company is chosen per table — one that actually has rows in it, ordered so the choice cannot move on a plan. Coverage 18 -> 103 with no seeding at all, and RLS is proved for several companies instead of one; the fifteen real gaps are NAMED on every run. O-3 is corrected in place with the original kept beside it. Forced red with the restore armed and proved non-empty first — waste_event_read set to using (true), both halves of the check fired by name, policy text compared byte for byte after
G28★The catalogue is writable from the appcompleteBuilt 2026-09-12 (D-197, D-198, D-199). F-001 items 4–7: POST /recipes, /recipes/:id/components, /dishes, /dishes/:id/recipes. The gap that mattered was asymmetric — a recipe could gain a method step and not an ingredient, and a recipe with no components costs 0.00 and declares no allergens while reading as complete. Verified in a browser end to end, every probe row deleted afterwards. ★ app typecheck was red on arrival: upstream's Table.tsx gained ../columnFit, which resolves against the CONSUMER'S tree — symlinked, seventh re-pin (D-198). U-009's dark ceiling raised 50 → 56 with the six new primary buttons named one by one (D-199). Battery 80/2/0/0, both failures that ceiling; dark re-ran green at 56, mobile dark could not be re-run at a sustained load near 900. ★ Closed out 2026-09-14: mobile dark measured clean at 390 px on the tree after it (59 allowed, D-206), and deployed to dev at 1d6d8d2, verified by the absence of 38a0681 and of the previous bundle
G29★What could be created and never correctedcompleteBuilt 2026-09-12 (D-200 to D-204). F-001 items 8–14: three PATCH edits and four reads nothing opened. ★ Point 8 closed 2026-09-14: the upstream diff was read before the eighth re-pin (D-205), the full battery ran at ff1f388 — 80/2/0/0, both failures the U-009 ceiling that G29's three drawer submits had crossed — and the ceiling was raised by name, 56 → 59, with both dark runs clean after (D-206). Every other point was measured: all seven controls exercised in a browser; verify-money, verify-tokens and the reachability sweep exit 0 on 2026-09-13. ★ Reading the handler found D-200 — a rename cleared choose_count, which the plan divides by — forced red by reverting the fix. Building the gate history consumed one of the sweep's own controls (D-201). The browser pass found D-203, a drawer that said *"no rule"* while it was still asking. Deployed to dev at 1d6d8d2, verified by the absence of 38a0681
G30★Stock can be received and moved, and a batch recorded, from the appcompleteBuilt 2026-09-14 (D-207). F-001 items 18–20: POST /stock/receive, POST /stock/movements, POST /batches — three writes into APPEND-ONLY tables, now reachable from Stock and from each prep-list row. Known gaps 19 → 16. ★ Reading the handlers before building found four refusals that were missing or unreadable, each measured on the running API before and after: a receive-line quantity of "Infinity" answered 500 and now 400 with a sentence; a waste typed positive would have added stock to a shelf the food had left, so the sign now follows the kind and a receipt or a transfer is sent to its own door; a duplicate batch code and a two-thing component answered with constraint names and now answer with sentences. ★ The batch quantity starts empty, with the plan as a hint, and door checks are three-state. 11 unit tests, each rule forced red by name — one sabotage refused itself because its anchor occurred three times. Browser pass 13/13, its one console error attributed to the deliberate duplicate. U-009 59 → 63, the four named. Battery 82/0/0/0 at 11ad37e. Deployed to dev at 00133fa — a peer's docs-only commit above G30 — verified by the absence of 1d6d8d2 and of the previous bundle
G31★A tab can be opened and run, and a ticket raised and filledcompleteBuilt 2026-09-14 (D-208). F-001 items 21, 22, 25 and 26: POST /tabs and POST /tabs/:id/lines from Takings, POST /moments/:id/tickets and POST /tickets/:id/items from the service moment. Known gaps 16 → 12. ★ Three refusals on smallint columns, measured before and after: a ticket line of 0 answered 409 with a constraint name, and a line of 40000 or a firing position of 70000 answered 500; all three are 400 with a sentence now. ★ A tab line sends no VAT rate and a ticket no firing position — the server decides both. 7 unit tests, each guard forced red by name. Browser pass 11/11: a 2 × 1250 dish line froze 5 % with its basis, a −500 adjustment was accepted, a −100 corkage was blocked before any request, a ticket was raised at the next position, a dish was routed and said so, a duplicate was refused with the count held, the service pass showed the ticket, and the only error was the deliberate 400. U-009 63 → 65, the two named. Battery 82/0/0/0 at b60b78f. Deployed to dev at b60b78f, verified by the absence of 00133fa and of the previous bundle. Opened Q-015: dish_station() routed a cook-chill beef bourguignon to the chiller
G32★Standing orders and delivery runs, created and moved from OrderscompleteBuilt 2026-09-14 (D-209). F-001 items 27–30: POST /order-schedules, POST /order-schedules/:id/lines, POST /delivery-runs, POST /delivery-runs/:id/status. Known gaps 12 → 8. Both panels render always now — they rendered only when rows existed, so the first of either had no control to create it. ★ The Pause control built on 2026-08-28 had never shown a pause: it reloaded the orders, not the standing orders, so the API held is_paused=true while the row read *"✓ Running"*. Found by the browser pass with a screenshot, fixed, re-measured. ★ scheduleInterval(): 0 and 60 weeks answered 409 with a constraint name and now 400 with a sentence; 3 unit tests, forced red by name. Seven weekday checkboxes are labelled as one group after the label audit refused the first draft twice — once for a label aimed at an id it could not see, once for my own comment. Browser: a standing order saved with Monday and Thursday, a line whose 5 % basis was said, a pause that now shows, a run moved planned → loading → out → complete, a complete run offering no further step, a run cancelled in place, zero error responses. U-009 65 → 68. Battery on 8b8fc31 81/1/0/0 — the one failure design parity, from event.clinic W449 moving a file Mise does not compile, re-pinned in 242afe8 (D-210). On 242afe8 78/0/4/0: wrangler's ProxyController crash at 02:18:41Z took the API away mid-run, and the four browser checks it blocked were re-measured on that commit one at a time — dark 1440 px and mobile dark each exactly 68, mobile light and the output contract clean. Deployed to dev at 242afe8, verified by the health commit exactly, the absence of b60b78f and of the previous bundle, and all three G32 phrases in the served bundle against a control phrase that is not
G33★A tender entered, a dish priced on a card, a venue kitchen added, an imported moment confirmed — and a correction that no longer unblocks a venuecompleteBuilt 2026-09-14 (D-211, D-212, D-213). F-001 items 16, 17, 23 and 24: POST /moments/:id/confirm, POST /rate-cards/:id/lines, POST /venue-kitchens, POST /bids; item 15, POST /auth/oidc, moved to EXEMPT with its reason read from oidc.ts. Known gaps 8 → 3, all three the owner's. ★ correctFact unblocked a blocking fact on every correction made in the app — tests written first and failed by name against the handler as it stood; fixed in the handler, and *Correct* now says whether the fact still stops work. ★ rateLineDish(): a dish price names its dish. 9 unit tests, both guards forced red by name. Browser: 11 checks — a tender recorded and opened, a price line whose basis was said, a dish-less line refused with residue zero, a venue kitchen added and opened, a blocking fact re-measured and still blocking and then deliberately unblocked, an imported moment confirmed and a second confirmation refused — zero error responses. U-009 68 → 70, the two named. Battery on 433f473 79/0/3/0: a hung API, killed by dev-api.sh at 06:04:28Z, blocked three browser checks, and each was re-measured alone on that commit — mobile light and the output contract clean, mobile dark exactly 70. Deployed to dev at 433f473, verified by the health commit exactly, the absence of 242afe8 and of the previous bundle, and all six G33 phrases in the served bundle — each absent from the G32 bundle — against a control phrase that is not
G34★The empty tenant tables: five filled, one refused, nine namedcompleteBuilt 2026-09-14 (D-214). The handover's third item. Tenant tables with no tenant row 14 → 10, measured by policy-shape-test.sh before and after; the ten are F-002's nine and outbox, deliberately not seeded. Three seeders in seed-demo.mjs, each guarded on its own state and selecting by name: a payment request for the short tab — the third seeder found dead below an early return — whose creation also writes its event; the demo's venue kitchen, created by name, with a disagreement and a correction that re-confirms a fact the way it was learned; and a pending inbound payload carrying no personal data. ★ Two premises measured wrong and corrected: a second, withdrawn payment request was built to reach payment_event and removed once the first run showed creation already writes it; and the correction had re-stated a venue-told name as *measured*, fixed in the seeder and restored on the demo through the product's own correction route. The first run also reconciled HACCP evidence — 4 records and a corrective action on append-only tables. The corrected seeder, run again, wrote zero data rows. ★ The first battery, on dbad4f6, failed *every screen renders* and the summariser discarded why; the check passed alone minutes later, and the discarded reason is D-215. Battery 82/0/0/0 at 09559f4. Pushed to master; the seeder runs against a local database, so nothing was deployed
SiteMarketing sitecomplete5 pages, same tokens, served on 4322

What “done” means here

Done means a verification script exited zero on the committed tree. Not that somebody opened the screen and it looked right, and not that an agent reported success. Every slice marked complete above has a script in scripts/ that exits 0 against the code as committed; a slice whose script has not exited 0 in a single run is not complete here, whatever else was built. Exit 2 means BLOCKED — nothing was measured, which is not a pass.

Run every gate at once:
./scripts/verify-all.sh local

Run it locally

# 1 · API worker, through its supervisor (Hyperdrive → local Postgres)
cd ~/Documents/mise && ( ./scripts/dev-api.sh > /tmp/mise-api.log 2>&1 & )   # :8801
# 2 · console app
cd ~/Documents/mise/app && npm run dev      # :5181
# 3 · marketing site (optional)
cd ~/Documents/mise/site && npm run dev     # :4322
# prove the tree before believing any status above
cd ~/Documents/mise && ./scripts/verify-all.sh local

Mise uses hash routing, so every link below is a genuine deep link — it survives a reload and can be pasted to someone else.

Screens with data

Screen
Open at
What is on it
Dashboard
landing
http://localhost:5181/#/
Company-wide counts and the next service moments
Functions
list + detail
http://localhost:5181/#/functions
Every booked function; detail screens hang off each row
Enquiries
list + detail
http://localhost:5181/#/enquiries
Enquiry pipeline with options and their expiry
Tastings
list
http://localhost:5181/#/tastings
Scheduled tastings against enquiries
Orders
list + detail
http://localhost:5181/#/orders
Drop-off and online orders — Phase B, mostly empty
Channels
empty by design
http://localhost:5181/#/channels
Ordering channels — Phase B, not started
Menus
list + detail
http://localhost:5181/#/menus
Menu structure; each menu opens its own detail
Dishes
list + detail
http://localhost:5181/#/dishes
Dishes with equipment rules per (dish × style)
Recipes
list + detail
http://localhost:5181/#/recipes
Nested sub-recipes, per-step yield, the cycle guard
Products
list
http://localhost:5181/#/products
Suppliers, dated prices, declared allergens with source
Kitchens
list
http://localhost:5181/#/kitchens
Counted named resources, capacity windows, storage bands
Equipment
list
http://localhost:5181/#/equipment
Equipment and the rules that bind it to dishes
Playbook
signature
http://localhost:5181/#/playbook
★ Facts with provenance and expiry derived from a volatility class
Crew
list
http://localhost:5181/#/crew
One labour engine for quote and timesheet; double-booking refused by the DB
Production
list + detail
http://localhost:5181/#/production
Backward scheduling, prep ≠ pack, batches with lot traceability
Stock
list
http://localhost:5181/#/stock
Append-only movement ledger with no stored balance
Compliance
list
http://localhost:5181/#/compliance
HACCP as tasks; a missing record counts as a failure
Clients
list
http://localhost:5181/#/clients
Client records behind the enquiries
System
diagnostic
http://localhost:5181/#/system
Health fingerprint and configuration
★ The Pass
needs an id
open from Production → a batch
Full-bleed gloved-hand service surface. Leaves the shell entirely and needs a service-moment id — open it from a function's detail screen
BEO / quote / settlement detail
needs an id
open from Functions
All three are id-addressed (/#/functions/<id>/beo, /#/quotes/<id>, /#/settlements/<id>)