Temporary workforce · build status

Verbunk

Crew requirements, availability, assignment and proof of delivery. Generated from the build ledger at build time.

generated 2026-09-27 20:59 UTC tree 4b4e05a 54 slices from docs/BUILD-LEDGER.md

Overall

100%complete
54 complete 54 total

Enquiries from the website

Not measured. This page can show how many enquiries are waiting, but only if the reader key is in the environment when it is generated. It is deliberately not stored in this repository.

VERBUNK_LEAD_KEY=your-phrase python3 scripts/build-status.py --out dist-status

The phrase is the one set on the Worker with npx wrangler secret put LEAD_KEY. If that has never been run, run it first — nothing can read the enquiries until it has, including this page.

Until then this is not a claim that none have arrived — it is a statement that nobody asked.

Phases

Phase 1 — Foundation

V1 – V9 · 9 slices

9 complete

Phase 2 — The delivery spine

V10 – V17 · 8 slices

8 complete

Phase 3 — The event day

V18 – V22 · 5 slices

5 complete

Phase 4 — Scale and intelligence

V23 – V29 · 7 slices

7 complete

Phase 5 — Compliance, money, insight

V30 – V34 · 5 slices

5 complete

Phase 6 — Native clients

V35 · 1 slice

1 complete

Design migration and harness work (unnumbered)

V16a – V31a · 19 slices

19 complete

Every module

SliceNameStatusThe ledger’s note
Phase 1 — Foundation
V1Repository scaffold
a13d742 · 2 Aug 2026
complete35 checks green
V2Database foundation and RLS harness
cc922e6 · 2 Aug 2026
complete34 checks green; local + Neon dev
V3Identity, session and RBAC
b42d7cb · 2 Aug 2026
complete95 checks green; 8 + 9 extra points all met
V4Console shell and design system
900c11d · 2 Aug 2026
complete44 checks green; design parity 33 with 12 sabotage cases
V5Client accounts
2e7458d · 2 Aug 2026
complete90 checks green; 10 sabotage cases; signup + invitations landed here
V6Worker profiles
f5e1bc3 · 3 Aug 2026
complete87 checks green; 11 sabotage cases; fixed verify-V3, broken since V5
V7Documents and credentials
2d135bd · 3 Aug 2026
complete122 checks green; 16 sabotage cases (5 did not fire and were rewritten); 0011 + 0012
V8Expiry blocking
b8fc838 · 3 Aug 2026
completeRe-verified on the committed tree in run 8: sections 1–8 green, 0 failures. Flag cleared
V9Configuration packs
2a4ee6e · 3 Aug 2026
complete0014–0017; chain 93/0 including section 10; V3 109/0, V5 96/0; policy shape 372/0
Phase 2 — The delivery spine
V10Events and workspace
ca6615d + 653532a · 3 Aug 2026
complete72 checks green, 0 failed, in ONE clean run (detached, PGID 47610). The product is ca6615d; the run that proved it is the second SHA. The §6 illegal-move assertion is fixed and OBSERVED; all nine points met
V11Requirement builder
be5e15a · 4 Aug 2026
complete88 passed, 0 failed, exit 0 — watched to completion. Pay rule set pinned per requirement to the existing frozen config version rather than gaining a second version number; charge_rate in integer minor units; break policy two columns
V12Availability
91fc439 · 4 Aug 2026
complete99 passed, 0 failed, exit 0 — watched to completion. All 13 sabotages caught, including a new one for the flywheel rule itself. Applying run 18's draft found six defects in it, two of which meant it could never have been applied or even parsed. Local + Neon dev
V13Assignment core and audit spine
6f292ac · 4 Aug 2026
complete99 passed, 0 failed, exit 0 — watched to completion. 0023; planned retired; app_rw holds no UPDATE on status/is_reserve, so the transition function is the only path and it writes both rows. Reserve promotion is a timestamped history row that changes no state. A sabotage found the matrix could not see a scheduler being given the worker's own move
V14Crew planning board
2702fbc · 4 Aug 2026
complete116 passed, 0 failed, exit 0 — watched to completion. 0024; the Control Room REPLACES V13's crew list at the same route. Every fill figure comes from GET /v1/events/:id/readiness and neither renderer computes one — the seam V17 replaces. 10 matrix sabotages, 2 type sabotages, 7 WEB-suite sabotages. Keyboard handlers unit-tested; end-to-end keyboard is a MANUAL check
V15Conflict detection
c95e25b + a24dc80 · 12 Aug 2026
complete80 passed, 0 failed, exit 0 — watched to completion. 0028-0031. Detection is one function called from a TRIGGER, because §88 says a check outside the transaction is a race. Working-time ceilings are per-country config app_rw may read and not write. credential_gap severity is deliberately NOT configurable — a warning there would switch off V8's gate. travel_time is recorded undetectable: no coordinate exists, measured. Three inherited fixtures modelled impossible rosters and were fixed rather than the guard weakened
V16Offers, acceptance, worker app shell
9242a17 + a00dbd9 · 13 Aug 2026
complete96 passed, 0 failed, exit 0 — watched to completion. 0032-0034. A worker IS an external actor, so app.event and app.requirement scoped every venue, zone, call time and break to NULL — found by the first script here to hold a WORKER session, and fixed with one narrow SECURITY DEFINER read rather than a wider policy, because widening exposes charge_rate_minor and budget_minor. Buttons said what you become, not what you do; action_label is per edge, NOT NULL, and V14's board reads it too. A call time is an OFFSET, never a timestamp. A V13 fixture had been passing on the TIME OF DAY for three slices, its strongest assertion vacuously
V17★Readiness Score
212c53e + 4ea1155 · 12 Aug 2026
complete59 passed, 0 failed, exit 0 — watched to completion. 0025, 0026, 0027. The model is COLUMNS on assignment_status and criticality_level, not a CASE — and the NOT NULL caught expired, which §V17.2's table omits. 6 of §V17.3's 8 blockers built; the two credential codes named and deferred rather than faked. 8 sabotages caught. The product is 212c53e; the run that proved it is the second SHA
Phase 3 — The event day
V18Briefing builder and acknowledgement
1ac44b3 + bd65913 · 14 Aug 2026
complete98 passed, 0 failed, exit 0 — watched to completion. All ELEVEN sabotages caught, and the unsabotaged splice still passes. 0043-0046 on both databases; fifteen inherited children re-run, 1157 checks. A briefing is a FROZEN pack template plus a requirement override that is deliberately not frozen; the merge is one security definer function so no route decides it. An acknowledgement names the material_version_no it read and is immutable, and its INSERT policy pins acknowledged_by to the caller, so a scheduler cannot acknowledge for somebody else holding every permission. The matrix caught the gate short-circuiting across a state change — a condition copied from V8 without its reason — so briefed -> checked_in was never gated and a stale acknowledgement carried somebody into work; 0046 closes it. Carries D3 muster_at, mutually exclusive with call_offset_minutes, and D8 venue coordinates, which turn V15's travel_time from a recorded gap into a firing check with haversine and no PostGIS. D7a is mine: the acknowledgement stops counting and the worker KEEPS the booking, because only apply_assignment_transition may write a status — recorded for the owner to overrule. Writing 0045 I rewrote two functions from memory rather than splicing and every assertion passed because none called them
V19Check-in and check-out
0b8824e + fed88a0 · 14 Aug 2026
complete126 passed, 0 failed, exit 0 — watched to completion. All FOURTEEN sabotages caught. 0047-0052 on both databases; sixteen inherited children re-run, 1244 checks. 0014 had built checkin_method and checkin_policy sixteen migrations earlier and nothing referenced the policy — measured — so V19's first job was the pin, which goes on the REQUIREMENT for 0019's pay-rule reason. Evidence is append-only, written only by app.record_attendance behind FORCE RLS with no INSERT policy, and stores the geofence verdict and never the coordinates — my ruling, in the morning questions. A reading COARSER than the radius is refused, because a geofence with unbounded accuracy always passes. A geofence at a venue with no coordinates refuses rather than silently downgrading. A LATE arrival is never refused — only the early side, and only for arrivals. Also repaired call_offset_minutes and muster_at, which had no UPDATE grant for fifteen and two migrations, so a call time could be set once and never changed. Four defects, none visible to a migration: prose passed where a reason CODE was expected; §A7's supervisor granted by one function and refused by the one below it; the gate DEADLOCKING a shift that records no presence, caught only by V13's older fixture; and a vocabulary chosen by array index. The sabotage battery was itself wrong twice — two sabotages hit seeded pack data instead of fixtures and the freeze guard's refusal read as 'caught'
V20Live ops console and change log
afb6ba1 + a8a096b · 14 Aug 2026
complete114 passed, 0 failed, exit 0 — watched to completion. All FOURTEEN sabotages caught. 0053-0055 on both databases; seventeen inherited children re-run, 1362 checks. The operational window is derived from TIME, because app.event_status has no today-is-the-day state. Five edges must say why inside it — named as data, and deliberately NOT declined or any check-in. is_dark means nobody present AND the muster passed, so an empty post before its call time is EARLY and one of three present is SHORT. The change log unifies the transition and audit tables, bounded at both ends, and never returns the audit payload. The console polls every 3s paced by COMPLETION, pauses on a hidden tab, keeps the last board when a poll fails, and always says when it last succeeded — §B3's other half. Five of the fourteen sabotages found the MATRIX rather than the code, including a leak test that could only fire if a leak was already present. Also found a cross-tenant read in its own new functions, an A1 guard that behaved differently on the two databases, point 7's route list living in FIVE places, and a chain that wedged 43 minutes on a psql with no timeout
V21Timesheets and approval
141a611 · 15 Aug 2026
complete159 passed, 0 failed, exit 0 — watched to completion. All FIFTEEN sabotages caught, and the unsabotaged splice still passes. 0056-0058 on both databases; eighteen inherited children re-run, 1463 checks. 0056 is D11's fallback_method — null still refuses, which is 0049 unchanged, and the trigger is the VENUE having no point and never a device declining to share one. A timesheet resolves hours_from_presence through the pack version the requirement PINNED and snapshots that version, so republishing a pack cannot rewrite how a worked shift is paid; the matrix republishes with the flag flipped and requires both the stored row and a fresh submission to be unmoved. Time is stored as an INTERVAL, so D12's exact minutes have no rounding step at all. Nothing is editable: no UPDATE or DELETE grant plus a trigger, so "approved timesheets are immutable" is a consequence rather than a second rule, and a correction is a new row. The two-step chain, its permissions and which step moves the assignment are all ROWS. Four of the fifteen sabotages found the matrix rather than the code, all failure form 12; the first run's eight failures were the erasure fixture naming its own gap and 0056 shadowing a V19 sabotage — 0058 is that fix
V22★Proof Pack
f2d4aec · 15 Aug 2026
complete190 passed, 0 failed, exit 0 — watched to completion. All NINETEEN sabotages caught, and the unsabotaged splice still passes. 0059-0060 on both databases; nineteen inherited children re-run, 1609 checks. Sections 1-9 of the document are assembled in SQL, so determinism is a property of the query rather than of the code: every timestamp goes through app.proof_ts and no JavaScript Date exists on the path, which is what makes §6.7's two-timezone requirement unable to fail for a reason nobody thought of. The audit chain's leaves are hashed IN THE DATABASE because a leaf covers before/after and a requirement.pay_basis_changed payload IS a charge rate — the leaf list travels inside the pack so the root is recomputable from the document alone, and §10 says out loud that a leaf is not. No personal name is ever in the signed document: everybody is W-<12 hex> from app.proof_subject, which hashes the worker id WITH the org id, and the names live beside the pack outside everything hashed — so an Article 17 erasure deletes a row the signature never covered and the matrix proves it by deleting a worker and requiring the Merkle root not to move. The signature binds content_sha256 as well as the root, which 08-… §2.2 does not ask for and without which it would attest rows nobody can see attached to an unsigned narrative — recorded as a deviation. §8 states that it cannot evidence equal treatment rather than being silently empty, and every section carries a sentence for its own emptiness. The inherited battery found the one defect this slice shipped: proof_pack.content was a bare jsonb, and verify-policy-shape.sh's census refused it on both databases — an encoded string is valid jsonb and would sign and verify perfectly while not being the document anybody thinks it is, on an immutable row with no later repair. 0060 is that fix
Phase 4 — Scale and intelligence
V23Pay rules engine, and a payment provider
2ab82ec..0064 · 15 Aug 2026
completeCOMPLETE: 230 passed, 0 failed, watched to exit 0 on 15 Aug, with verify-policy-shape.sh at 398 passed, 0 failed immediately before it. The clean run was taken only after the stale verify-V8.sh (pid 25696) was allowed to exit on its own and the grant it borrows was PROVED restored — information_schema.column_privileges returned 0 rows for app_rw UPDATE on worker_credential.expires_on — because a result measured on a weakened database is not a result. Earlier in the slice: §1-§10 GREEN at 207 passed, 0 failed, all TWENTY-THREE sabotages caught. §11 then found a REAL regression and it is fixed. 0061-0064 on local and Neon dev with identical checksums. The regression: sandbox_rail reported declined, which is also a row in app.assignment_status — V8's vocabulary guard reads that taxonomy from the database and greps source for the quoted form of every value, so it fired three slices after it was written. 0064 renames the four sandbox statuses to upper case (the convention file_export already set) and adds a trigger so no provider status may ever equal one of our own vocabulary ids again. The guard was NOT weakened and no per-file exemption was added. WHAT REMAINS: one clean bash scripts/verify-V23.sh, watched to exit. Nothing needs rebuilding
V24Client portal and live client view
0066-0070 + 332f34b · 15 Aug 2026
completeRun 6 on 15 Aug: 184 passed, 0 failed, watched to exit 0 - twenty-one inherited children green inside it, all thirteen matrix sabotages caught, all four render sabotages caught by an assertion that names the rule, 0066-0070 applied to local and Neon dev with identical checksums. All THIRTEEN of 18-... section 8 are now asserted. Run 41 built the data layer and the API and stopped there, green and explicitly incomplete; run 42 built the screens. Data layer: client_approver beside client_viewer, app.client_account_settings defaulting closed, client_approval, client_change_request, the append-only client_portal_view, notification_intent for V25 to drain, ten app.portal_* functions behind one door, and eight /v1/portal/* endpoints. Screens: S6 programme, S1 readiness, the wall, S2 roster, S3 changes, S4 approvals, S5 documents, plus web/src/design/portal.css - the fifth per-surface token set that 11-... section 9.1 has asked for since V10 and that nothing could see was missing. S7 Account is deferred to V25 by decision (MORNING-2026-08-15.md section 2.33): every control on it writes a preference V25 owns. Checks 8 and 11 are covered by scripts/lib/portal-render.mjs, which bundles the real component and renders it with react-dom/server - an assertion over MARKUP, not over a helper or a grep. Two new reference endpoints so no client-facing word is spelled in the browser: /v1/reference/portal-seat-states and /v1/reference/client-decisions. Reads published state via roster_version, never a scheduler working state - except the live operational window. Writes notification INTENTS and sends nothing
V25Notification engine
03253d9 · 15 Aug 2026
completeRun 2 on 15 Aug: 173 passed, 0 failed, watched to exit 0 - twenty-three inherited children green inside it, all TWENTY-SEVEN sabotages caught, 0071-0074 applied to local and Neon dev with identical checksums. Both roadmap verify lines asserted. Four channels as ROWS with one enabled, because one has an adapter and the provider credential is the owner's; the drain never queues for a channel it cannot deliver on. Quiet hours are wall-clock, per person per organisation, following 0021's convention exactly - quiet_to < quiet_from crosses midnight and equality is the whole day, never 00:00-23:59, which is failure form 11's missing minute. A held message is DEFERRED with a stored release instant and released as a NEW row superseding the hold, so the ledger can still say it was held; a whole-day window suppresses rather than deferring to a moment that never comes. app.notification_delivery is append-only with a receipt superseding, holds NO address, NO rendered body and NO name - a delivery pins a template VERSION and the message is reproduced from it, so an Article 17 erasure never has to edit an immutable row. No notification path can report whether a person exists: the unsubscribe function returns void (no value can be differenced against a second call), the self-scoped reads take no argument at all, the drain returns four integers, and the endpoint answers a byte-identical 200 to a valid token, a used one, a malformed one and an absent one - proved over HTTP, four ways, with an audit row written in every branch and naming nobody. S7 Account built here per MORNING-2026-08-15.md section 2.33. FOUR FINDINGS WORTH MORE THAN THE SLICE. (1) Not one of the 27 sabotages had ever been applied - sabotage_patch matched on pg_get_function_identity_arguments, which includes PARAMETER NAMES, so every patch raised its own refusal and the matrix then failed for the reason the sabotage itself failed: reported as 25 of 27 caught. With the matcher fixed one sabotage was genuinely uncaught, the daytime branch of notification_is_quiet closed at both ends, invisible to the crossing-midnight assertion because the two branches are separate expressions. (2) The identical identity_arguments mistake twice in one hour with opposite consequences - 0074's own assertion made it and REFUSED TO APPLY the migration; the sabotage matcher made it and reported twenty-five catches. An assertion in front of the work fails loudly; one beside it lies. (3) app.drain_notifications joined app.org_member on m.role_id, a column that does not exist - two of the three seeded kinds address a role, so every one would have failed with 42703 in the endpoint whose job is to send, and 0071's self-check could not see it because the rule-A1 guard raises before the query. 0073 repairs it and executes all six of the body's queries past the guard. (4) verify-V6.sh's surface-prefix guard had been a false positive for nineteen slices, stripping /console/ only, so any nav entry below /portal or /app could never match a screen - and nobody could have known, because none had ever pointed there. Also: a trap fires AFTER finish, so run 1's abandoned signup organisation poisoned eight children before its own teardown ran; audit_event.org_id is RESTRICT on purpose, so section 8.5 now runs the two-statement erasure order BEFORE the battery and MEASURES that it took
V26Auto-match, ranking, explainability
d16b98a · 15 Aug 2026
completeRun 5 on 15 Aug: 168 passed, 0 failed, watched to exit 0 - twenty-four inherited children green inside it, all TWENTY-TWO sabotages caught, 0075-0077 applied to local and Neon dev with identical checksums. The roadmap's verify line is asserted in the matrix and again over real HTTP. The arithmetic the slice turns on: an unmeasurable factor is dropped from the numerator AND the denominator, never scored zero. 37 876 venues carry no coordinates and no worker has a home centroid until somebody types one in; scored zero for absence, a candidate two kilometres from an unlocated venue ranks below one ninety kilometres away and nothing on any screen would ever say so. The matrix ranks one bench against two venues differing only in whether they have a point and requires the order not to move. Cost is a named factor and is deliberately empty: the only per-worker cost this schema holds is wage_age_band, a statutory minimum that varies BY AGE, so a cost factor computed from it is age discrimination with a decimal point on it - the factor says so in the worker's own explanation, and the migration and the script both assert against pg_get_functiondef of the INSTALLED function that the column name is nowhere in it. Reliability comes from V19's attendance evidence, counted only over requirements that pin a check-in policy, because no_show is not distinguishable from declined by any property on assignment_status and V28 owns the column that would tell them apart. Every excluded candidate is written down with a reason, a sentence and no score at all - *why was I not offered this* is the question the directive is really about, and a candidate who never appears cannot be answered. The override asks for a permission and for nothing else: not the run's age, not the candidate having been ranked, not the candidate not having been excluded. Four append-only tables, no write policy on any, no personal name on any, retain_until at 24 months stored per run. FOUR FINDINGS. (1) 0075 granted UPDATE on two new app.worker columns and not INSERT, and that table's privileges are held PER COLUMN - so from the moment it applied, creating a worker was refused for everybody, 403 on a route V26 has nothing to do with. Invisible to the isolation matrix, which inserts as the owner; six inherited children found it at once. 0077 is the fix and asserts it as a SET DIFFERENCE so a column added next year is covered. (2) The sabotage battery's first pass reported FIVE of twenty-two NOT CAUGHT and every one was the MATRIX at fault - no candidate could reach the branch the sabotage broke, twice; an assertion comparing SCORES where the defect was in the ORDER; a control nothing had ever asked a question; and one caught by a check constraint on a neighbouring column rather than by the trigger under test. (3) unavailable was already an HTTP error code in api/src/errors.ts - the fourth vocabulary collision in four slices and the first not in the database at all, so 0075's structural diff over pg_class and its collision trigger both worked perfectly and both looked in the wrong place. 0076 renames it. (4) Two defects the gallery found that no test could see: a candidate a human had REMOVED sat at rank 2 behind a badge reading *A person decided*, and in partial the override controls vanished leaving a tidy page with its human-review route silently absent - the state Directive 2024/2831 makes illegal, indistinguishable from everything working. And the run cost four attempts: run 1 was corrupted because I EDITED THE EXECUTING SCRIPT after misreading a head-truncated ps as the run having died
V27Offer waves and reserves
0078-0081 + c04a5d4 · 15 Aug 2026
completeRun 4 on 15 Aug: 179 passed, 0 failed, watched to exit 0 - twenty-five inherited children green inside it, all TWENTY-TWO sabotages caught, 0078-0081 applied to local and Neon dev. Both roadmap verify lines asserted: the matrix fires one plan twice at instants nine hours apart and requires every deadline to be identical, and the promotion is reconstructed from assignment_history alone. A tier's deadline is derived from its PLANNED opening, never from the moment the scheduler got round to running it, so a cascade run four hours late gives nobody four extra hours; the cascade refuses an instant that has not happened yet, because now() inside a transaction is frozen at its start and a handset's clock is settable by its owner. The escalation asks is_commitment and not fills_headcount - offered fills a seat and commits nobody. SIX FINDINGS, THREE OF THEM WORTH MORE THAN THE SLICE. (1) 0078's grant select withheld nothing: ALTER DEFAULT PRIVILEGES had already given app_rw all four verbs, and under FORCE RLS a write the grant permits but no policy admits affects no rows and raises nothing - indistinguishable from a refusal. 0079 is the REVOKE. The same sweep found twelve tables from five earlier slices in the same state; left alone deliberately and now V27a. (2) 0078's policy had no TO clause, so it belonged to PUBLIC including app_auth. 0080. (3) open collided with "open" in board-keys.ts and portal.ts - the fifth vocabulary collision in five slices and the SECOND in TypeScript rather than the database, so 0075's pg_class diff and its collision trigger both worked perfectly and both looked in the wrong place. 0081 renames it; the collision check must sweep the TypeScript vocabularies too. (4) FAILURE FORM 9, TWICE MORE, and one was hiding a genuinely uncaught sabotage. An assert-false inside a begin/exception block raises a message its own handler then greps; the collision guard's message contained the phrase its handler looked for, so deleting the guard entirely reported as caught. Spotting these one at a time is how there came to be two, so scripts/lib/form9.mjs now sweeps all 19 message/needle pairs with a planted collision proving it can fire. (5) api's npm test ran node --test TWICE, so every reader took the second summary: twenty inherited children saw 6 where their floor was 154 to 245, in five different phrasings because each script implements the read itself. One invocation, one summary, 301. The floors were never wrong. (6) Two defects only the gallery could see: the standby sentence printed a raw vocabulary id as Standby pays 25.00 GBP per_shift, because basisLabel fell back to the id and the gallery served no /v1/reference/rate-bases; and the failed cell said *Nobody is held in reserve* and *This post offers no standby payment* after the read had been REFUSED - two confident statements of fact about a question it had been told it may not ask, identical to the truth and the shape Directive 2024/2831 makes illegal. And a third the gallery DISPROVED: the cascade table's last column sits at x=541 in a 375px viewport, which reads as clipped and is not - .data-table is display:block; overflow-x:auto, so scrolling it puts Accepted at x=329. Four runs: run 1 void (the owner edited site/css/styles.css mid-battery), run 2 149/28, run 3 178/1, run 4 green
V28No-show and late management
0082-0083 + 0f0c0bf · 15 Aug 2026
completeRun 2 on 15 Aug: 206 passed, 0 failed, watched to exit 0 - twenty-six inherited children green inside it, all THIRTY-THREE sabotages caught, 0082-0083 applied to local and Neon dev with identical checksums. The roadmap's verify line - *reliability impact is explainable and appealable* - is asserted as both halves: the score returns one row PER FACTOR and never a bare number, so a caller physically cannot render the figure without the sentences that produced it; and an upheld appeal removes the shift from the numerator AND the denominator, in V28's score and in V26's ranking. THE APPEAL HAS NO FREE TEXT ANYWHERE AND THAT IS THE DESIGN. The honest sentence somebody types into *why did you miss this shift* is health data, permanently, in a table more than one role can read, collected from the population least able to object - so an appeal carries a GROUND from a closed list of five whose last entry says somebody will ring you. Asserted as a SHAPE over pg_attribute on both databases, not trusted from the design, and swept for four field names in the three TypeScript files. THE ROUTE IS A SCREEN A WORKER CAN REACH: worker.record on the worker surface, in the worker nav, authorised on the caller BEING the worker before it is authorised on a coordinator's permission - a human-review route only a coordinator can walk is the rubber stamp the directive forbids, and it renders as a completely ordinary page. SILENCE IS NOT ABSENCE: a post pinning no check-in policy is skipped entirely, because counting its silence would mark every remote and unmonitored worker absent. Lateness is derived from the call time and from nothing else, confined to app.event_operational_window whose two ends coalesce, and requires_reason_in_window is still true on exactly FIVE edges. FIVE FINDINGS. (1) The ladder marked the person it had just called up as running late, in the same transaction: it walks stages most-serious-first, so it confirms an absence, promotes the reserve, then reaches the lateness stage where that reserve is now a non-reserve commitment ninety minutes past a call time. 0083 is the repair, and it keys on the PROMOTION rather than on created_at, because a rule whose behaviour depends on how long ago a row was written is a rule nobody can reason about. Invisible to every structural check in 0082 and to the unit tests; the matrix caught it only because it counts AUDIT ROWS rather than asserting a particular row exists. (2) Point 7's guard had only ever been asked one question. It knows two ways to satisfy *every mutating endpoint writes an audit_event*: call writeTenantAudit, or be read-only. reliability.ts is the first file in twenty-eight whose audit rows are all written by the security definer functions it calls, and it failed in V28's own section and then again inside V25, V26 and V27. The fix REDIRECTS the check rather than waiving it - the file names its functions and each named function's INSTALLED body must contain app.audit_event - and it was proved able to fail by pointing it at worker_reliability, which writes none. (3) The escalation read 500ed on a missing join, and the gallery could not have seen it: the fixture supplies stage_label and tone, so both halves agreed with each other while both disagreed with SQL nobody had run. Failure form 17 with a missing join instead of a missing wrapper. (4) The erasure fixture did not populate the two new tables and the instrument built for that named both itself. reliability_appeal is the one that matters - a worker's challenge to an automated decision about their livelihood that cannot be erased is a GDPR failure sitting inside the feature that exists for fairness. Both populated; 63 tenant tables erase clean; neither carries an immutability trigger because both are append-only by REVOKE, which is V27a's lesson paying for itself. (5) V28 broke one of V26's sabotages and the harness said so honestly. 0082 taught rank_candidates about an upheld appeal and pushed ) as presence_shifts three lines from the predicate V26's sabotage aims at; sabotage_patch raised SABOTAGE MATCHED NOTHING and the battery reported FALSE CATCH, which before V25's rebuild would have read as a catch and left V26's denominator unguarded from the day V28 landed. Re-pointed at the current body, not deleted and not loosened - then EVERY sabotage_patch IN THE CORPUS WAS AUDITED AGAINST THE INSTALLED BODIES — 86 invocations across every verify script, all matching exactly once, which is the general closure of V25's finding: a pattern that matches ZERO times is a false catch and a pattern that matches by luck is worse, and the census is the only thing that answers both at once for a corpus rather than for the one script somebody happened to look at bar one deliberate double in V28 that must break both sides of a fraction. AND TWO CONTROLS HAVE NO BEHAVIOURAL SABOTAGE, both recorded rather than deleted (failure form 16, twice, for two different reasons): the nameless-author refusal is unreachable because a session naming nobody holds no role and is refused by the permission check one line earlier; and the score denominator's measurable filter cannot be separated from the numerator, because with two coupled factors punctuality is unmeasurable only when attendance is zero. Both asserted structurally with the reason beside them, and the matrix's own nameless-session assertion now requires the message that ACTUALLY fires rather than accepting either of two, which was failure form 6 wearing an or
V29★Risk Radar
0084-0086 + cbaf127 · 16 Aug 2026
completeRun 2 on 16 Aug: 181 passed, 0 failed, watched to exit 0 on its own top-level PID - twenty-seven inherited children green inside it, all THIRTY-TWO sabotages caught, 0084-0086 applied to local and Neon dev with identical checksums. THE GATE, AND THE CAVEAT IS PART OF THE RESULT RATHER THAN A FOOTNOTE. 08-... V29.6.1 says a model that cannot beat *assume last month's fill rate* must not ship. First execution, untuned, identical under TZ=UTC, Pacific/Auckland and Europe/Budapest and identical on local Postgres 16 and Neon dev: history month fill rate 0.6429, test set 28 requirements, BASELINE MAE 0.9287 seats of shortfall per requirement, MODEL MAE 0.5357, pass by 42.3 per cent. BOTH DATABASES HOLD 0 ORGANISATIONS, 0 EVENTS, 0 REQUIREMENTS AND 0 ASSIGNMENTS, so *last month's fill rate* is undefined against real history and the backtest runs over seeded fixtures with the baseline drawn from the same fixtures as the model. That is a materially weaker gate than V29.7 describes and it sits close to failure form 17 - a model and its baseline sharing their author's assumptions. What separates it: the baseline reads only the history month's aggregate rate and the model reads only per-requirement features, and neither can see the other's inputs. The only claim a pass supports is that the weighting arithmetic recovers signal deliberately placed in pool_depth and daypart_penalty. It is NOT evidence that the forecast is good in production. THEREFORE A RELEASE CONDITION, recorded in FORWARD-PLAN.md: the backtest must be re-run against REAL history before Risk Radar is shown to a paying customer, and must beat the naive baseline on that data. The ten weights were set from 08-... V29.3's own ordering BEFORE the backtest file existed and were not touched afterwards; the gate passed on its first execution, and had it failed the failure would be in this row and the module would not have shipped. Six append-only tables, no write policy on any, no worker_id and no personal name anywhere in the slice - the model reads people and stores counts and averages, which is what lets retention be a plain 24 months. Weights, features, actions, confidence levels and prohibited signals are all ROWS; no vocabulary value is spelled in api/src or web/src, swept against the database and against the TypeScript vocabularies. Two of the ten features can never be measured in this build and are ROWS rather than omissions: historical_dropout needs a venue record and this product has only a venue NAME typed on an event, weather_risk needs a forecast source. Both leave the DENOMINATOR rather than scoring zero - V26's rule, and it matters more here, because scoring absence zero would report every post in the product as calmer than it is, uniformly, with nothing anywhere saying so. confidence is low whenever the organisation has no completed work, unconditionally, and only the lowest level carries allowed_without_history. The contributions sum EXACTLY to the score - one expression, asserted by the function that writes it and again in the matrix. expected_gain is the model's own arithmetic rearranged, derived from the remedied factor's contribution, so an action cannot promise a gain the model does not believe. The override asks for a permission and for nothing else and applying a recommended action IS an override row, so V29.5's *single audited operation* and V29.7.7's human-review route are one mechanism rather than two that can disagree. FIVE FINDINGS, AND THE TWO THAT MATTER WERE FOUND BY LOOKING RATHER THAN BY TESTING. (1) Reading the model's real output found two recommendations nobody could take: promote_reserve offered against a post with no reserves, and extend_confirmed against a post whose only occupant held an unanswered OFFER - HANDOFF.md section 6's warning that offered fills headcount and commits nobody, arriving in a new place. Every assertion available was about the recommendation's VALUE and the defect was in its APPLICABILITY. 0085 makes both preconditions COLUMNS on the action row. (2) The gallery showed the WHY column holding all ten sentences in a table cell on every row, and over-capacity is forty rows - which is precisely the wall V26's own comment warns about, and it passed every test. 0086 adds headline, the measured term carrying the largest share of that score, chosen in SQL beside the score it summarises rather than cut out of a stored string by the browser. (3) Run 1 was 136 passed, 38 failed and EIGHT of thirty-two sabotages went uncaught, every one the same disease: an assertion whose subject could only take one value in the fixture. Org C could measure three terms and medium needs five; the only past post was filled 1 of 1 so its forecast, outcome and error were all zero and replacing the error with a constant zero was invisible; the only early post was 05:00 London while the runner sat an hour ahead, so 06:00 was still early. The fixture now carries a rival event, a past post that went short with a reserve on it, and two posts straddling the early-start boundary at 06:30 and 07:30 London with the assertion on the DIFFERENCE between them. Two sabotages were re-pointed rather than deleted, both failure form 16 - the tenant half of the post scan cannot be reached because no two organisations share an event id, and the rounding residual only bites when the denominator divides badly - and both are asserted against the installed body in section 5.5 with the reason beside them. (4) A COMMENT naming a job role broke four children, because assert_no_hardcoded_taxonomy is shared and does not strip comments. Sixth instance of failure form 9 in six slices; the prose was rewritten and the general fix is an OPEN OWNER QUESTION at MORNING-2026-08-15.md section 2.82, not made tonight. (5) The erasure fixture did not populate V29's six tables and the instrument named all six itself; all six refuse DELETE, so all six are 0019's shape and what was missing was any row proving the exemption reachable. And a false red of my own: the gate was reported as FAILING under TZ=Pacific/Auckland when it had passed - a zone name contains a slash and the log path named a directory that does not exist, which is the class HANDOFF.md section 5 ends on. 8851 is the LAST FREE PORT in the band 8850-8869; all twenty are now claimed and the next script needing a dev server must widen VERIFY_PORT_BAND_HI. Decisions in MORNING-2026-08-15.md sections 2.75-2.87
Phase 5 — Compliance, money, insight
V30Compliance centre
0088 + d5db84e · 17 Aug 2026
completeRun 3 on 17 Aug: 148 passed, 0 failed, watched to exit 0 on its own top-level PID — thirty-four inherited children green inside it, all EIGHTEEN sabotages caught, 0088 applied to local and Neon dev with identical checksums. THIS SLICE ARRIVED AS CODE RATHER THAN AS A SLICE. 0088, api/src/routes/compliance.ts and web/src/screens/Compliance.tsx were committed by a run that a spend limit ended mid-slice, and nothing had ever run against any of them — no battery, no sabotage, no inherited chain. The verification was therefore written by somebody who had not written the code, as an adversary, which is an advantage available exactly once. RUN 1 WAS 133 passed, 15 failed AND EVERY FAILURE WAS REAL. (1) The screen rendered with NO STYLING AT ALL: it opened <div className="screen"><header className="screen-head"> and neither class has a rule anywhere in web/src/design. Nine inherited children caught it instantly; no other screen uses either word or uses <header> at all. A third spelling of an existing class is not a new class. (2) A screen path collided with a role id: path: "compliance" where compliance IS an app.org_role, and screens.ts already named worker — two bare role ids in one file, which V5 reads as a vocabulary in the browser. The convention was already in the file and simply not followed (console.workers is workers). Now compliance-centre, which is also what the roadmap calls the screen. (3) A V29 SABOTAGE'S CATCHABILITY DEPENDED ON THE CALENDAR — caught on 16 Aug, NOT caught on 17 Aug, with no change to V29: the fixture's shifts are current_date + 7, the weekday moves nightly, and the Sunday penalty decides whether round(naive, 2) changes anything. A check that is green on some days for no reason anybody will reconstruct. It now halves every term. naive + 0.01 was tried first and was caught by a CHECK CONSTRAINT rather than by the assertion it was aimed at — the same lesson V30's own appeal sabotage learned an hour earlier, where a planted column called explanation was caught by the completeness sweep that keys on that name and had to be renamed worker_comment to reach §6. RUN 2 WAS 147 passed, 1 failed and the one was a FIFTEEN-SECOND deadline in verify-V7.sh on wrangler's log forwarding, which passed standalone ninety seconds later on the same tree: a timing assumption that holds on an idle machine and fails under the load the script is designed to run in. Raised to sixty. It cost minutes rather than hours only because that check's own message already said *a harness race, not a product failure*. THE VERIFY LINE THE ROADMAP CALLS ITS BEST IS NOW PROVED IN BOTH DIRECTIONS AND ON LIVE TABLES. 0088 proved it against a scratch table it creates and drops, which proves the FUNCTION refuses and nothing about the product's own tables. §1 of scripts/sql/v30-compliance-isolation.sql writes the prohibited sentence into app.briefing_template.duties and app.config_pack_version.notes under set local role app_rw, through the ordinary tenant policy, as a member holding config.manage — emotional state, religious belief and pregnancy each refused with the class NAMED in the message, neither refused row present afterwards, and an ordinary operational briefing STORED and read back. That last is the assertion that costs something: a guard that refused everything would satisfy every negative assertion ever written about it and stop the product working, and the eighteenth sabotage puts disabled — a word 0088 REJECTED by name because it is in every venue access note — back into a pattern, so the only check that can notice is the positive control. §1.1 rules out the other reading, that app_rw simply cannot write those columns, by writing the same column in the same session with the offending words removed. TWO CLAIMS 0088 MAKES THAT NOBODY HAD CHECKED. The register resolves every FUNCTION and RELATION it names through the catalogue and says nothing about the five PERMISSIONS app.compliance_register() offers in a CASE expression — a typo there yields an empty reviewer list that reads as *nobody holds this permission*. §4.1 resolves all five against app.permission, requires each to be held by some role, and asks through the function so it measures what the screen is handed. And the migration's header says the live trigger is proved *over HTTP*; it is not, deliberately — the claim is about the database, so it is proved at the database, which is strictly stronger than a Worker's answer about a Worker. THE REGISTER IS ASSERTED AGAINST THE INSTALLED FUNCTIONS AND NEVER AGAINST prosrc. Zero defects from automated_system_register_defects() and zero unregistered decision relations on BOTH databases, asked of the running catalogue rather than only inside the migration — R3's half. §2 is a set difference between the triggers Postgres is actually running and the rows claiming they exist, in both directions, plus §2.1 on the TG_ARGV argument list, because a guard on the right table with the wrong columns is invisible to any check that asks about tables. §3 requires the free-text register to equal the catalogue in both directions: 66 columns, not the 62 the migration's prose says — recorded as documentation drift rather than fixed, because the file is applied on two databases and its checksum is compared. F7 IS ONE QUEUE OVER THREE MECHANISMS AND ALL THREE BRANCHES FACE A FOREIGN TENANT. Both organisations hold one row in each of reliability_appeal, match_override and risk_override, every foreign id is a LITERAL from the fixture, and the question is asked from both sides — a filter accidentally written against one org's id passes half of that. Rule A1 is proved on all three functions at once: a session with no organisation must RAISE, and only two of three refusing is a failure. No fourth review mechanism was built and that is the design; the queue's output signature is asserted to carry no name and no free text. FIVE FINDINGS AND THE GALLERY PRODUCED ONE OF THEM. Opened and read at 820px and full width: the empty cell returned reviewers: [] and drew *nobody here holds the permission* — a state this endpoint cannot produce, because compliance_reviewers is asked for compliance.read and refuses any caller who does not hold it, so a successful call always names at least the reader. The cell now draws the reader. The empty case IS real for the four other permissions the register names and the API asks for none of them — V31's. The screen's branch is kept with the reason beside it, and §5.5 proves the empty case reachable using portal.read, which exists and is held by client roles rather than org roles. Also recorded: the over-capacity cell is 1010 rows and 46,416 pixels tall, a ceiling this screen cannot reach, since the register's cardinality is bounded by the schema at 66 and grows by tens. Left rather than re-scaled, because changing a fixture so a screen looks better is how a gallery stops being evidence. NO DEV SERVER AND NO PORT, and the trade was made rather than dodged: VERIFY_PORT_BAND_HI is still 8869. V31 will have to widen it — a GDPR toolkit is mutating endpoints and cannot be proved from the database alone. Decisions in MORNING-2026-08-15.md §2.94-2.98
V31GDPR toolkit and working-time guardrails
0089-0094 + 38725a1 · 17 Aug 2026
completeRun 3 on 17 Aug: 227 passed, 0 failed, verify-V31 PASSED, watched to exit on its own top-level PID 47499 — thirty inherited children green inside it, V23 at 220, V24 at 170, V28 at 184, V30 at 115. Run 1 was 195 passed, 32 failed; run 2 was 225 passed, 2 failed, and both of run 2's failures were one thing: 0094 had moved the code out from under its own sabotage. The daily-ceiling regression patched an accumulator line that 0094 replaced with a range_agg merge, so it matched nothing — reported as a FALSE CATCH rather than a catch, which is the machinery V25 rebuilt after 27 sabotages sat unfired for a slice. Re-pointed at the body that ships: drop the union and a day's overlapping spans count twice, the exact defect 0094 repaired and the one V15's fixture caught. Pattern verified to match the installed function exactly once before committing. Three findings outlive the slice. A refusal that RAISES rolls back its own audit row — every erasure this product declined would have left no trace, and 0091 returns a document instead, which also makes it 409 rather than 403 because the caller did nothing wrong. A fixture with no OVERLAP cannot tell a sum from a union. And eleven retention rows said 2555 days under a basis sentence citing six years, which is 2190 — found by reading the gallery, because every assertion compares the schedule against the catalogue and the catalogue has no opinion about how long six years is. verify-V7.sh passed inside runs 2 and 3, which retires the WIP label on 1ab4d16: the invite token is now re-keyed in the database rather than harvested from an asynchronously forwarded log.
V32Invoicing, margin, payroll export
0099-0100 + af11ce4 · 18 Aug 2026
completeRun 3: 210 passed, 0 failed, verify-V32 PASSED, watched to exit on its own top-level PID 33947 — 37 inherited children green inside it, all 13 sabotages caught, 0099 and 0100 applied to local and Neon dev with byte-identical checksums. ★ THE VERIFY LINE'S FIRST HALF IS MET AND THE SECOND IS HONESTLY UNSATISFIED, in those words. Margin matches a hand-calculated fixture: scripts/sql/v32-margin.sql's header carries the arithmetic for four requirements in a comment so a reader can check it with a pencil without running anything — 113750/65625, 63000/23200 (a PER-SHIFT charge, times heads and not hours), 8829/5304 (both sides land mid-penny so truncation and rounding differ), and a fourth that is NOT PRICEABLE. Totals 185579 charged, 94129 costed, 91450 margin. The percentage is a RATIO OF SUMS and the fixture is built so the mean of the three per-row ratios answers 4847 against the correct 4928 — a fixture whose requirements all carried one rate would answer the same either way and the assertion would measure nothing. The unpriceable requirement is DROPPED FROM THE DENOMINATOR, never scored zero: scored zero it reads as a hundred per cent margin, the most attractive wrong number this product could show somebody setting a price, and its rule set carries a CONDITIONAL rate rather than none because that is the case a real agency builds. The bureau half names what it was built to and stops there: HM Revenue and Customs, RTI Data Items Guide 2025-26 (GB, comma) and Nemzeti Ado- es Vamhivatal 2508 kitoltesi utmutato 2025 (HU, semicolon), two countries and two delimiters so the exporter has been asked more than one question. Validation against a bureau's own loader has NOT happened and needs a bureau — an owner action, exactly as V22 waits on a production signing key and V23 on a provider account. The gap is a measured product fact rather than a sentence: four required columns those specifications need and this schema cannot produce — a date of birth, two tax identifiers and a tax code — are stored with a null source_key and a note saying why, reported by app.payroll_export_gaps(), shown on the screen and proved over HTTP. A file that looked complete and was unusable is the worse failure. ★ 0100 IS THREE DEFECTS THE MATRIX FOUND, AND THE FIRST IS THE ONE THAT MATTERED. 0099's blanket immutability trigger refused the organisation CASCADE, so an organisation holding one invoice could not be deleted at all — V31's erasure toolkit could not remove a tenant and erasure.sql could not complete. Article 17 defeated by an integrity guard, which is the portfolio's *cascade erasure is void if the parent is never deleted* arriving from the opposite direction. Narrowed to permit a delete only when the organisation is already gone: true for a cascade, false for anything anybody can type, and unspoofable by a definer because it is a fact about the world rather than about the caller. Both directions provoked on real rows inside 0100. Also: a derived reference of eight hex characters collided on the matrix's second invoice (now the whole id, unique because the primary key is, and still no per-org counter — a gap in an invoice sequence is a question an accountant may ask and nobody could answer); and currency was ambiguous inside event_margin_summary, which raised on every call while nothing in 0099 called it — an assertion beside the work rather than in front of it, one more time. RUN 1 WAS 152 passed, 53 failed AND FIFTY-THREE HAD FOUR CAUSES. The largest: strip_ts_comments cannot see inside a multi-line JSX block comment — it strips // lines and JSDoc * continuations, so a job-role word in the second line of a {/* … */} block is a LIVE string to the taxonomy guard, and one comment in Invoicing.tsx failed thirty-seven children at once. Reworded here and the stripper is left alone deliberately — editing an instrument thirty-eight scripts read while a battery runs through it is what voided V27a's run 1; the real fix is in MORNING-2026-08-15.md §2.112. Also: app.schema_migration keys on version and has no filename column; and the checksum was compared against shasum of the raw file when the runner stores sha256(ddlOnly(text)) so that prose can be corrected and DDL cannot — wrong by design, replaced with the comparison that means something, identical on both databases. A SABOTAGE WEAKER THAN THE CHECK IT AIMED AT, for the second time in this project. Removing finance.read from event_margin_summary alone was NOT CAUGHT — not a broken assertion, but the roll-up calls event_margin, whose refusal was still standing. Defence in depth working, and a case that proved nothing until re-aimed at both. Run 2 was 158 passed, 1 failed and the one was the end-to-end fixture publishing its config pack version BEFORE inserting the pay rule set, which V9's freeze correctly refuses. It also exposed that the early finish paths left a signed-up tenant behind — a backstop teardown now runs in the EXIT trap as well as the measured one before the battery. Two decisions to overrule if you disagree, in MORNING-2026-08-15.md §2.115: the margin is finance.read and never external, refused in the database, so a scheduler cannot open it; and an invoice line stores NO amount — it pins an immutable calculation and the total is summed through the pin, which honours V33's verify line a slice early. Screen console.events.invoicing, mounted and registered in the gallery in all five states with the hand-calculated figures.
V33Analytics and delivery scorecard
0101-0105 + e5ee5b0 · 18 Aug 2026
completeRun 8 on 18 Aug: 231 passed, 0 failed, verify-V33 PASSED, watched to exit on its own top-level PID 32517 — 38 inherited children green inside it, all 19 sabotages caught, 0101-0105 applied to local and Neon dev with identical checksums. ★ THE VERIFY LINE IS MET BY A CHECK THAT CAN FAIL RATHER THAN A RECOMPUTATION THAT AGREES WITH ITSELF. The schema was measured before anything was written, and the measurement decided the design: 0051's app.move_assignment makes THREE writes in one transaction — a transition row, the live status UPDATE, and an audit event carrying before and after — and app.record_assignment_creation writes a transition on INSERT, so the transition journal is COMPLETE and the audit journal covers every move but carries no state on assignment.created. So fill rate and no-show rate are computed from live state AND by folding the journal, two genuinely independent sources — one an UPDATE, the other an INSERT — and time-to-fill and reserve burn have NO live counterpart at all: nothing in app.assignment records when a seat was accepted or that a row was ever a reserve. That absence IS the argument for the slice, it is stated on the metric and drawn on the screen in words, and it is why neither may ever become a stored column. The journal is then pinned to the audit events MOVE FOR MOVE in both directions, matched on ORDINAL rather than on occurred_at because both are written in one transaction and comparing two clocks that agree to the microsecond fails on a slower machine. The second half — *no metric may be computed from a denormalised counter that could drift* — is app.denormalised_counter: eleven stored counts, measured from information_schema rather than remembered, each with the reason it is or is not safe, and app.metric_counter_defects() reading the INSTALLED body of every registered metric function. The register's argument is the part worth keeping: roster_version.covered_count is named the most tempting wrong source in this schema because the ROW IS IMMUTABLE AND THE FACT IS NOT — an assignment it counted can become a no-show an hour later and the number never moves. A guard forbidding all eleven would have been argued down; one that says why the groups differ survives. ★ FIVE MIGRATIONS, AND 0102 IS THE DEFECT OF THE SLICE. A security definer OWNED BY A SUPERUSER SEES EVERY TENANT: the scorecard divided by eighteen seats where the hand working says twelve, because 0101 leaned on row-level security and the migration runner connects as the database owner, which bypasses it. Thirty-two slices did not find this because every earlier definer takes an id — app.event_margin(p_event) — so its scope came from its argument; a scorecard takes a DATE RANGE and is the first function in this product whose scope is the tenant and nothing else. Explicit org_id = v_org on all six base relations, underneath the policies rather than instead of them, and verify-V33.sh counts the predicates in the installed bodies on both databases. 0102 also carries two more the matrix found: percentile_cont returns double precision, so time-to-fill raised on EVERY call while nothing called it — 0099's ambiguous currency in a third costume; and a move is not only a change of state, because assignment.reserve_promoted moves the reserve flag and not the status, so a correspondence keyed on the state alone reported a divergence for the one event V27's cascade produces most. 0103 AND 0105 ARE A PAIR AND THE SEQUENCE IS THE FINDING. verify-V30.sh failed V33 twice from one slice away with opposite messages. 0103 answered *registered prose with no guard* by installing the trigger — 0088's installer is a ONE-TIME loop, so every later slice adding a prose column must install its own. That satisfied V30 §2 and broke §3, which is the assertion that says what the register IS: free_text_field must EQUAL the text columns a TENANT can write. Neither new table is one — both carry a SELECT policy only and 0101 revoked the other three verbs by hand — so 0105 removes the rows and the triggers. A register that over-claims is as wrong as one that under-claims and it is the more comfortable mistake: it reads as thoroughness while reporting a control over a surface nobody can reach. 0105 proves its own premise rather than asserting it, making app_rw try both writes and be refused. 0104 CAME FROM OPENING THE GALLERY AND READING IT, for the fifth slice running. The reconciliation returns rows from three checks comparing three different pairs of records, in two columns called expected and actual, so the screen headed them *the journal says* and *the audit events say* — and a metric row reading 6667 and 5833 told a reader THE AUDIT EVENTS SAY 5833 when the journal said 5833 and the live rows said 6667. Every figure was right the whole time; a divergence report that misattributes the divergence sends somebody to fix the wrong side of it. Renamed to first_says/second_says with a compares column naming the pair in words. The gallery also caught the empty cell putting the fill-rate sentence under all five metrics when the product writes a different one per metric, and *both sources agree* claiming a reconciliation that never happened over an empty period. EIGHT RUNS, AND RUN 1's EIGHT MISSES WERE ONE THING: EVERY UNCAUGHT SABOTAGE WAS WEAKER THAN THE CHECK IT AIMED AT, because the matrix only ever ran against a healthy world. Four positive controls now break the world inside a sub-transaction and require the reconciliation to NAME the break — a live row that moved without its journal, a move with no audit event, an audit event with no move, and a metric reading a drifting counter — plus an external-actor control that GRANTS analytics.read to an external role and takes it away again (measured: app.actor_is_external() is decided by the org role's flag, so no external role holds it today and the clause could not otherwise be shown to bite), and an A1 assertion that names its own rule instead of accepting any refusal. The organisation-predicate sabotage was re-aimed after the event join's own predicate kept the neighbour out — V32's finance.read finding again. RUN 3 WAS 193 passed, 27 failed AND 25 OF THOSE WERE ONE STRAY DOUBLE QUOTE that left verify-V30.sh unparseable, from a scripted edit whose search string stopped one character short; the error named a sed line eighty lines below the cause. Also in run 3: a comment explaining a fix failed the check the fix satisfied — the note above defaultWindow quoted the millisecond expression verify-V12.sh forbids, and that guard is not comment-stripped. AND THE LAST TWO RUNS FOUND TWO HARNESS DEFECTS IN OTHER SLICES, BOTH REAL. verify-V24.sh reported that app.portal_roster HAD STOPPED SCOPING ITSELF — a security regression — because a bare psql | grep -q got no answer for one moment under thirty-eight nested scripts, a live Worker and two databases; it passed standalone at 194 passed, 0 failed on the same tree, and the read is now separated from the judgement. Then verify-V29.sh disagreed across databases with identical figures on two consecutive runs: libpq does not read TZ, so V29.6.1's *runs under UTC and under Pacific/Auckland* loop had been running the gate TWICE IN THE SERVER'S OWN TIMEZONE and reporting a figure stable across two zones it had never left — and this machine's Postgres defaults to Europe/Budapest while Neon defaults to UTC, so the cross-database equality compared two different calendars every night between the two midnights. PGOPTIONS='-c timezone=...' on all four call sites; measured, the same local database answers 2026-08-17 under UTC and 2026-08-18 under Pacific/Auckland and gives different MAEs, and under UTC it now matches dev exactly. V29's timezone-robustness requirement was measuring nothing for four slices and now measures something. Screen console.analytics, org-wide rather than per event, mounted and registered in the gallery in all five states with the hand-calculated figures. No mutating endpoint and that is the design: every figure is derived on read, and analytics.ts is on the single shared read-only list with its reason, re-verified on every run. Decisions in MORNING-2026-08-15.md §2.117-2.120
V34Offline resilience, PWA hardening, import
0106-0109 + 6fe1f1e · 18 Aug 2026
completeRun 4: 329 passed, 0 failed, verify-V34 PASSED, watched to exit on its own top-level PID. 39 inherited children green inside it, all 19 sabotages caught, 0106-0109 applied to local and Neon dev. THE ROADMAP IS FINISHED — V1-V34, every row complete. IndexedDB queue, a service worker that caches the shell and NEVER /v1, and CSV + two competitor formats as ROWS. **Conflict resolution favouring the earliest timestamp is answered as *earliest by WHICH clock*: a device instant is a CLAIM, admitted only between the moment check-in opened and the moment we received it, and app.clock_ruling says in a sentence which of the three cases decided. The queue is sorted by the admitted instant and recorded in that order, so the same three captures delivered FORWARDS and BACKWARDS produce identical evidence - both directions asserted, because one alone proves nothing. Standing evidence never moves (0048); the disagreement is measured in seconds and only the EARLIER one asks for a human. Import refuses any field the format catalogue does not declare - the only way an upstream taxonomy could arrive - and a competitor's job category is COUNTED, reported back and stored nowhere, because this product attaches a role to a SHIFT and not to a person. FIVE DEFECTS IN THE SLICE'S OWN FIRST PASS, and the first is the one the slice exists to prevent. (1) One bad capture lost the whole queue: the landing row's NOT NULL foreign key to app.assignment raised OUTSIDE the refusal handler, so a shift cancelled while its owner was underground aborted the batch. 0108 - nullable exactly when refused, resolved before the write, and the unverified id is not kept. (2) The erasure exemption asked for app.erasing, which nothing in this product sets - a tenant with one capture was UNDELETABLE. The portfolio has one discriminator, app.owning_organisation_is_erased, and 0016 says why. Found by erasure.sql's coverage census, not by reading. (3) V34 broke V33 and V33 was right: import_batch.row_count is a genuine cached aggregate of app.import_row - measured, one row inserted per line unconditionally - so it is REGISTERED and counter_drift() recounts it. (4) import_row holds a worker_id and had no retention answer; V31's own sweep refused the slice. 0109: two years, with the batch. (5) The Worker named two outcomes under a comment claiming it counted the catalogue's flag; the flag now travels on the row. AND THREE CHECKS THAT PASSED WHILE MEASURING SOMETHING ELSE**: the missing-accuracy sabotage left record_attendance's geofence refusal standing and was re-aimed at a pin_at_post shift where this guard is ALONE; the rule-A1 assertions counted refusals rather than naming the rule, so a deleted A1 guard read green because the permission check caught the caller one line later (V33's finding, second time); and the organisation-predicate threshold of two was both false for a one-table definer and blind to batch_org = v_org - now DERIVED, one predicate per tenant table the body names. See MORNING-2026-08-15.md sections 2.125-2.130
Phase 6 — Native clients
V35Token auth for native clients
0138 + 515611f · 31 Aug 2026
completeRun 69 on 31 Aug: verify-V35.sh 32 passed, 0 failed, inside a battery of 50 ran, 0 failed, 5 803 checks. A native client can authenticate. POST /v1/auth/login accepts "client": "native" and returns the session token in the body instead of a Set-Cookie; the Worker accepts it from Authorization: Bearer. NO JWT AND NO SECOND TOKEN TYPE — mintToken() already produced 256 bits from a CSPRNG, hashToken() already stored only the SHA-256, and resolveSession() already took a RAW TOKEN STRING, so the session model was never browser-specific and only its delivery was. 0138 adds one column, client_kind. A FIRST DRAFT OF THE MIGRATION CLAIMED THE CSRF DECISION IS ANCHORED IN THAT COLUMN AND IT IS NOT: checkCsrf() runs at step 2 of the request, deliberately BEFORE the session is resolved at step 3, so a forged request never costs a database round-trip — a column cannot inform a decision taken before the row is read. The real rule needs no column: skip the gate when a bearer is present AND NO COOKIE IS, because CSRF exists only against ambient credentials. The column's actual job is the other half: a browser session's token may not be replayed as a bearer, since it is delivered once in a __Host- HttpOnly cookie and never appears in a response body. app_auth cannot UPDATE the column, so a session cannot be promoted to escape that. The compiler found a fifth issueSession call site grepping missed (oidcCallback), and selectOrg carries the kind forward rather than hardcoding browser — demoting a native session there would fail it on a CSRF gate it cannot satisfy, visible only to multi-org users on a phone. THE VERIFY SCRIPT FAILED THREE TIMES AND ALL THREE WERE THE SCRIPT: /v1/me carries no requires, answers 200 to everybody and reports "authenticated": false for an anonymous caller, so asserting on its status passes identically for an acceptance and a refusal. Now two signals that must agree — a body field and a gated route. Each of §7's properties carries its own control, because a refusal proves nothing unless the same input succeeds through the other door.
Design migration and harness work (unnumbered)
—Console surface palette (part of the design migration)
4b58891 · 3 Aug 2026
completeparity 41/0, 19 sabotages; defined and enforced, not yet switched on — 3 edits remain
—Design migration — the palette switched on
882779d · 3 Aug 2026
completeparity 46/0, 22 sabotages; --ink-3 corrected; density lever completed; §7b makes "defined but unwired" unreachable
—Demo gallery (DoD point 9) + V3–V9 backfill
84fd272, 6d65011 · 3 Aug 2026
complete26 screens, 66 cells, 3 conditions; point 9 is now a typecheck error; verify-demo-absent 12/0 wired into V4
—Full-chain re-run + harness repair (run 14)
839e20e · 3 Aug 2026
completeChain found a real V9 failure the gallery slice introduced. Fixed, plus §7's three green-on-empty paths. 23 further audit findings listed in run 14
—Route-table wiring probe + the API route table (run 15)
ca6615d · 3 Aug 2026
completescripts/lib/route-wiring.mjs; DoD point 7 is a type; two source files found INVISIBLE to the taxonomy guard
—The NUL-byte guard, and the corpus-readability pattern (run 16)
653532a · 3 Aug 2026
completeassert_no_nul_byte_in_tracked_files runs in V1 and therefore in every cascade; arithmetic closed over 226 tracked files. The ledger itself carried a raw 0x00
—The site guard, and one run at a time (run 18)
e92df66, be5e15a · 4 Aug 2026
completeassert_site_untouched compared the working tree against main and the owner began committing to site/ on this branch — section 1 of every script failed and three verification runs were lost. There is no mechanical way to tell an agent's site edit from another session's; that was measured, not assumed. It now asserts nothing under site/ is STAGED — new, and the half that would have caught dc384af
—The verify chain de-duplicated (run 20)
d1ffdf1 · 4 Aug 2026
completerun_child_slices() in guards.sh; every script ran its ancestors at most once per chain. Measured with a paired control: verify-V4 66 checks/94 s bare, 64 checks/41 s de-duplicated, the two lost checks replaced by named skip lines
V16aIdentity verification
8ea62cc + 4a2f3b5 · 13 Aug 2026
complete111 passed, 0 failed, exit 0 — watched to completion. All THIRTEEN sabotages caught, and the unsabotaged splice still passes, so they failed for their own reasons. Reworked the same day by D6 and D7: a check is asked for only after somebody tries to take work, and the cheap tier is ours while the expensive one is billed. 0035-0038 on both databases; a 14-section matrix passes against both; 239 API + 223 web tests and design parity 48 all green. The gate refuses acceptance at the database on the SHIFT END horizon; a failed check opens a review task and never auto-rejects; a tenant cannot resolve its own crew as verified; the provider status map and the price list are data, so no adapter names our vocabulary or a cost. Two adapters, contract-tested. Run 26 built it and left it unverified; run 27 is the verification
V17aThe platform review desk
7a6be30 + 4661305 · 13 Aug 2026
complete74 passed, 0 failed, exit 0 — watched to completion. Six sabotages caught. 0041-0042; the first actor in this schema who belongs to no organisation. A conditional NOT NULL replaced a plain one and is stronger: a null org is permitted exactly for a platform role and forbidden for every other, AND a platform role with an org is forbidden too. requirePlatform refuses anyone who HAS an organisation, matching current_org() is null in the database, so staff cannot launder a decision through a tenant. The desk is on the console surface rather than a fifth one Owner decision, 13 Aug 2026. A platform actor (org_id IS NULL), a verification.review_platform permission and a console outside the tenant envelope. Exists because V16a closed the only door a wrongly-refused worker had: there is no platform-side human in this schema, and an agency clearing its own crew verifies nobody. Also gives V16a's review queue the screen it lacks. Touches V3's RBAC
V17bOvernight availability
7497e97 · 13 Aug 2026
complete45 passed, 0 failed, exit 0 — watched to completion. All FOUR sabotages caught. Fixtures and assertions only; the script asserts the migration count is unchanged so the slice cannot have added schema. Owner decision, 13 Aug 2026. Fixtures and assertions only, no new schema. Zero availability patterns in either database wrap past midnight, so app.expand_availability's wrap branch — whose own comment calls the night shift "not a corner case" — has never been entered by a fixture. Must assert an 18:00–02:00 pattern with a 22:00–06:00 shift inside it, the same across a DST boundary, and that marketplace_worker_is_free agrees with expand_availability
V23aRoster publication
0065 · 15 Aug 2026
completeCOMPLETE on a watched exit 0. 0065 on local and Neon dev with identical checksums. Two tables (roster_version + its manifest roster_version_item), one snapshot function app.publish_roster, one read function, one trigger on app.requirement and app.assignment, one button on the V14 board. The first watched run was 91 passed, 4 failed and every one of the four was the MATRIX at fault, not the code — the immutability sabotage never reached the trigger because app_rw holds no UPDATE grant (two controls, and a test of the outer proves nothing about the inner); the rule-A1 sabotage was satisfied by the permission check one line below it (failure form 6); the cover-cap sabotage had no over-filled post to work on and CANNOT have one, because app.guard_assignment_headcount refuses that write — so the cap is asserted against the function's shape; and one was a false catch on a syntax error, reported as such. superseded_by is DERIVED, not stored, because storing it would mean editing a published version — 0059's shape, and the matrix asserts the column does not exist. No personal name is snapshotted; the manifest holds worker_id and joins the name at read, so an Article 17 erasure never has to edit an immutable row. The dirty-flag trigger asks fills_headcount, never a status id, so booked -> checked_in does not mark a roster behind on the one morning the flag matters. Decisions in MORNING-2026-08-15.md §2.21-§2.25
V23bA fixture suite per Tier-1 vertical
2d9290d · 1 Sep 2026
complete13-… §7.1, the one check of §7's ten V23 shipped without. Run 70 found both V23 headers claiming nine of ten checks were in the matrix while eight were, corrected the sentence and left the coverage unbuilt. Seven fixture suites in scripts/sql/v23b-vertical-fixtures.sql, one per Tier-1 vertical, each asserting the arithmetic its own §4 quirk produces: a worker-week spanning three fixtures (sport, §4.4); a clock window that opened the previous calendar day and a Saturday uplift that does not follow a shift across midnight (festivals, §3); a call-out that costs what the booking cost, asserted against a colleague who worked it out (conferences, §4.3); travel money in band 400 that the band-200 premium cannot reach (brand activation, §3); a split shift, a statutory floor that bites on the short leg only, and a tronc the engine cannot express (hospitality, §4.2 §4.1 §4.6); a bench that is owed something (venues, §4.5); and a graded rate everything downstream scales (crew, §4.4). NO MIGRATION AND NO PRODUCT CODE — the engine is V23's and unchanged; §7.1 asks whether anybody has looked. ★ The vertical list is READ FROM app.vertical at run time, so an eighth Tier-1 row fails V23b rather than being quietly uncovered — a list written into the check is what the header this closes WAS. ★ And each vertical owns a sabotage its OWN suite refused: the first draft filed the role_in regression under the crew, the bench suite three sections earlier refused it first, the crew suite never ran, and the harness printed *caught*. Ten sabotages, each naming a phrase unique to one suite. 69 checks. Two hospitality findings out of the build, both open and both the owner's
V23cThe tronc, and the pay engine's blindness to it
f883437 · 1 Sep 2026
complete13-… §4.6, the destination half of a sentence whose prohibition half had been true and free since V23. "Must not pass through the pay engine" is satisfied by ABSENCE — V23b proved it by asking the vocabularies, and no configuration could reach for an effect that does not exist — so every run agreed with it and nothing could disagree. "Record as a separate distribution, referenced by the timesheet" needed a table and there was none. 0140 builds app.tronc_scheme, app.tronc_distribution and app.tronc_distribution_state, all definer-written and append-only with the correction chain every money table here already uses. Two tables rather than the one the finding sized, because is_independent_of_employer on every distribution row is a fact two rows of one arrangement can differ on — 0099 decision 1's hazard arriving at a boolean, and the boolean is the one a tribunal asks about. The troncmaster is an identifier mechanically: a CHECK forbids spaces, so TRONC-BANQ-2026 passes and a person's name cannot be stored — one constraint, and it is what keeps both tables out of the erasure walk and out of V30's free-text register. No worker_id, which is §4.6's own word and also a catalogue fact: app.personal_data_relations() finds relations BY that column and v31-privacy.sql §8 requires the retention schedule to have exactly one row per relation it finds. ★ The check that matters is behavioural and a vocabulary sweep is not it — v23c-tronc.sql §4 prices a shift, records a distribution of 500 000 minor units against that same timesheet, prices it again, and requires the total, the line count and every line's amount and running total to be identical; a tronc_minor column added to app.pay_calculation later passes the sweep and fails this. ★ And scripts/sql/erasure.sql caught 0140 before it shipped, with the sentence it has printed since 0130: a table is usually added together with the guard trigger that breaks erasure. guard_tronc_distribution_immutable was modelled on the invoice guard, whose only exemption is the whole tenant going — but a distribution references app.timesheet ON DELETE CASCADE, so an Article 17 erasure of a WORKER reaches it while the organisation still exists, and one tronc row made every worker in that tenant unerasable. 0141 adds the clause guard_pay_calculation_immutable has carried all along. The sabotage that failed to fail is the part to remember: restoring the broken guard and re-running erasure.sql PASSES, because that matrix erases an organisation — the census found the gap and could not test it, so v23c-tronc.sql §9 is where the claim lives. ★ And 0142 came from asking pg_trigger what app.pay_calculation has that this table did not: a chain guard. 0140's constraints restricted supersedes_id to the same organisation and to one correction per row, and nothing to the same shift or the same arrangement — so a correction against Saturday could supersede Friday's share, and Friday's figure would leave the head of the chain with nobody deciding. guard_pay_calculation_chain's defect one table over, and a trigger rather than a CHECK because a CHECK cannot see another row. 94 checks, 10 sabotages each caught by the section that owns it. Three migrations for one slice, and two of them are the slice's own mistakes — neither found by re-reading 0140, one by a standing census firing on the new table and one by comparing it against the table it was modelled on. Not built, deliberately: a worker cannot see their own share, measured in its own finding rather than papered over with a policy no control reaches
V27aThe vestigial INSERT grants
0087 + 354cc33 · 16 Aug 2026
completeRun 2 on 16 Aug: 137 passed, 0 failed, watched to exit 0 on its own top-level PID - thirty-three inherited children green inside it, all ELEVEN sabotages caught, 0087 applied to local and Neon dev with identical checksums. The twelve tables were re-measured from the catalogue before anything changed and the sweep returns exactly the twelve on this row, at table level and per column, on both databases. Two owner rulings were built FIRST, inside this slice, so this battery verifies them - MORNING-2026-08-15.md §2.89 asked for exactly that. (1) The org clock: whenIn() renders the organisation's clock with the reader's in brackets and DROPS the bracket where the two zones agree, which is the ordinary single-country case and the space mitigation HANDOFF.md §6 asked for; all five times on the Risk Radar go through it, and the WHEN column now says 05:00 over a sentence saying 05:00. No endpoint changed: /v1/me has carried active_org.timezone since V3, so the plumbing half of that ruling was a measurement away from being unnecessary. (2) The taxonomy guard scans a comment-stripped copy with a planted-live-string control inside the guard, on every call in every script - the owner ruled against opt-in because two guards that can disagree about one rule is R3's hazard inside the harness - proved in three directions: a stripper that removes everything, one that removes nothing, and a job role planted in real web/src. 0087 revokes the twelve and installs a SET DIFFERENCE over every policied row-level-secured table - {verbs app_rw holds} - {verbs a policy admits} = {} - asked per COLUMN as well as per table for INSERT and UPDATE and at table level for DELETE, because Postgres has no column-level DELETE and has_any_column_privilege(...,'DELETE') RAISES. TRUNCATE is locked to nowhere in the schema, which is free today and total if it ever slips. The migration carries its own positive control INSIDE the transaction: it grants INSERT back, requires the sweep to name it, and revokes it again, because otherwise "the answer is empty" and "the question is broken" are the same output. §3 of the matrix is the slice in one measurement: it creates an ordinary tenant-scoped INSERT policy on app.payment_batch and attempts the same write twice under it - refused FOR A PRIVILEGE with the message asserted, then, with the vestigial grant restored, the same write lands. The policy is identical in both halves and only the grant moved. THE SABOTAGES ARE PRIVILEGE CHANGES, and this is the one place the patch-a-function-never-data rule inverts: the subject IS a privilege, there is no function to patch, and GRANT is transactional so the closing ROLLBACK restores every one even on a run that dies. sabotage_grant refuses when the role already holds the verb, which is sabotage_patch's *MATCHED NOTHING* exactly. Two of the eleven are worth naming: one CREATES A THIRTEENTH TABLE - RLS forced, one SELECT policy, no REVOKE, as a future slice will write it - because a claim that a thirteenth cannot appear can only be tested by making one; and one REVOKES SELECT, without which §4 of the matrix is decoration. FIVE FINDINGS, AND FOUR ARE ABOUT THE HARNESS. (1) The policy-shape manifest was asserting the presence of the defect. APPEND_ONLY|app.roster_version claims app_rw HAS INSERT; it never did - publish_roster is a definer function - so that line passed for four slices *because of the vestigial grant* and failed the instant it went, on both databases, calling two working tables unusable. A shape manifest can only assert the shapes it has words for, and a missing word is filled by the nearest wrong one. DEFINER_WRITTEN is the new word: SELECT yes, no write verb at all, per column as well as per table. (2) strip_ts_comments | grep -q reports 141 under pipefail, because grep -q exits at the first match and sed takes SIGPIPE - so a check whose subject IS PRESENT fails. A new shape of failure form 1 and the first that produces a false RED; five sibling call sites in V25-V29 were the same shape and green by luck. strip_ts_grep removes the pipe, and || true would have "fixed" it into the original failure form 1. (3) strip_ts_comments was defined SEVEN times and strip_sql_comments twice; now one copy in guards.sh with assert_no_local_harness_rule_copies asserting the empty set, which matters more today because the shared taxonomy guard uses the same stripper and a local redefinition wins silently. (4) The dev-server leak of §2.91 is closed and HUP was never the hole: stop_worker walks the tree by PID depth-first, collecting children BEFORE the parent dies, because a parent killed first reparents its children to PID 1 - which is how the four orphans came to exist. Proved on a real npm exec tree, port free immediately after, every other project's listener unchanged. And twelve pkill -f calls were removed from eleven scripts: they existed to cover for the leak, against a standing absolute rule, in the repository whose handoff records killing another project's dev server twice. (5) The zone renders as an OFFSET rather than an abbreviation - GMT+1, not BST - because timeZoneName: "short" asks the reader's own locale; recorded as a deviation rather than fixed, since pinning a locale would render the DATE in a foreign format on every screen. Run 1 was VOID and I did it to myself: I found finding (1) in its output and fixed the manifest while thirty-three children were still reading it, so it measured two different trees. Let to finish rather than killed, and discarded. This slice starts no dev server and takes no port - the band is full (§2.80), it adds no endpoint, and widening the band for a section that would only be scenery is the wrong trade
V31aThe night-shift working-time gap
0095-0098 + 20a5bba · 17 Aug 2026
complete105 passed, 0 failed, verify-V31a PASSED, watched to exit on its own top-level PID 82988 — with verify-V31.sh green inside it at 227 checks and 35 of its own children, and all 11 sabotages caught. The last failing run was 103 passed, 1 failed and the defect was the SCRIPT'S OWN. Its teardown lived only in the EXIT trap, and a trap fires after finish, so V31a's signup tenant was still alive when §9 ran verify-V31 -> verify-V2, which asserts no organisation rows exist. The failure was reported four levels down against two scripts that had done nothing wrong. §8.5 now tears the tenant down BEFORE the battery and measured_cleanup proves it took — cleanup at the end protects the next run; only cleanup before the battery protects this one, which is V25's lesson arriving one level up. Four migrations, and three of them are defects the slice's own fixture and the inherited chain found rather than anything planned. 0096: the calendar day was the SESSION'S, not the organisation's — psql runs Europe/Budapest here and a Worker runs UTC, so one roster got two different legal verdicts depending on who asked, and the Worker's was wrong for every organisation this product is sold to; the matrix hands a Budapest and a Dublin organisation identical instants and requires 18 and 17, where a hardcoded UTC gives both 4 and 16. 0097: a guard on the right table watching the wrong columns — the guard FUNCTION learned to re-check hours_worked and the TRIGGER never called it, so a refused booking could be created by editing one number afterwards. 0098: V30's automated-decision register named a signature that no longer resolved, caught from two slices away on both databases. Also found: GateCheck was mounted by no screen for twenty-three slices, and verify-V1.sh through verify-V9.sh had lost their executable bit and were answering 126 Permission denied — nine scripts reporting as failures that never ran. The daily-ceiling sabotage stopped matching for the second time in two slices, so §5.0 now requires every sabotage pattern to occur EXACTLY ONCE: a pattern matching twice is invisible to false-catch reporting.
—The §-reference sweeps and §4F (run 73)
882959a, 02f268f · 2 Sep 2026
completeTwo sweeps §3 named, and the first found the defect it was written for inside run 72's own resolver. assert_every_self_reference_resolves walks the bare §N in every script header against that script's section "N." labels union its matrices' -- N. labels — 145 references over 36 scripts, the half of run 70's V7 defect the sibling never covered. ★ THE LETTER IS PART OF THE LABEL: run 72's resolver read §8B as §8, which exists, so it would have declared the V7 defect resolved and its clean 148-over-0 would have been the evidence; the sibling read 16-…md §6a as §6 the same way. Three more things were wrong about where a label lives — (?![0-9]) does not keep §4 out of §4.6 (24 of 169 truncated), a matrix is found by its PATH not a MATRIX= variable, and a matrix sub-case label is INDENTED. ★ AND THE SIBLING'S CORPUS WAS THE HARNESS, NOT THE PRODUCT — *"108 references across 95 files"* read as a sentence about this repository and described scripts/ alone; api/db/migrations, api/src and web/src held 255 more and thirteen were broken, six of them LINE NUMBERS wearing a section sign (12-…md §88 is line 88; the section is §3). Now 363 over 412, none broken, and twelve corrected — the thirteenth cannot be, because 0032 line 254 sits inside a do $$ … $$ block and migrate.mjs hashes the DDL, so editing it changes the checksum and the runner refuses on all three databases; the walk skips dollar-quoted regions of APPLIED migrations only. verify-design-parity.sh §4F walks the twelve className={…} expressions §4E's regex cannot see and found board-cell-person, named on every assigned board cell and declared nowhere while .board-cell-vacancy fourteen lines above it has a rule; its reverse direction found that §5 checked .chip-$tone and never .notice-$tone. Thirteen sabotages, two of them negative controls — a dotted §4.99, which belongs to the sibling and must NOT fire, and a broken label inside a migration's dollar-quote, which must not either — and verify-design-parity.sh §8 now carries 29. ★ AND A RESTORE WROTE THE WRONG FILE BACK: /tmp/Pay.orig and /tmp/pay.orig are one file on this Mac, the guard still passed on the swapped file, and only the typecheck caught it
—The three sweeps, and a corpus wrong one layer down (run 74)
6c491da, 4c9862f, c62fb46 · 2 Sep 2026
completeAll three sweeps §3 named, and every one found a check that covered less than it claimed. assert_every_self_reference_resolves widened from headers to whole files — 145 references over 36 scripts to 448 over 46, five label sources, three narrowings excusing nine references, inside the same one ok line. ★ THE ANCHOR WAS SKIPPING A WHOLE FILE: verify-design-parity.sh guards every section "N." with a sandbox test on the same line, so a column-anchored reader found none and skipped its 38 references — and with the file skipped the corpus is still 410 over 45, so neither the 400 nor the 40 floor moves. Only a floor on files SKIPPED catches it. Two defects: verify-V10.sh:866 said §7c on the line after the one naming V4, and verify-V15.sh:314 still said §88 — the pointer that started run 73's largest thread and survived it because it is BARE. ★ THEN THE SIBLING'S CORPUS WAS SIX GLOBS PRETENDING TO BE A PRODUCT. Run 73 widened it to 412 files; the files those globs missed held 29 more unwalked references, and the one that mattered was scripts/lib/guards.sh, the file that DEFINES the corpus — carrying a broken HANDOFF.md §14 that guards.sh itself RECORDS as found and fixed in verify-V34.sh. *A fix applied to the corpus is not applied to the file that defines it.* Now every source file git ls-files reports: 414 references over 510 files. ★ AND A CEILING, derived from the docs at 30, catching a §N no document could have: five more line-numbers-wearing-a-section-sign that name no document and nothing had walked — four §88 and an §118 whose own list cites §3.5 correctly four lines below. A heading numbered with a LEADING ZERO is a migration id, and the ledger's ### 0110 — takes the ceiling to 110 and lets every one through — sabotaged that way, and no count falls, which is why the derivation is printed. ★ TWO OF THE FIVE COULD NOT BE CORRECTED AND THE HASH SAID SO: all 142 migrations hashed with ddlOnly imported from the runner, before and after; 0028 and 0030 moved because those lines sit inside do $$ … $$ bodies. **Second sweep — the 83 uncounted app.* function comments. actor_supervises_assignment said "two callers" and has three, a fourth counted claim run 72's count sweep could not see because it counted raise exception statements. And rule R8 is written in three places and was broken in the one nobody walked: compliance_reviewers joined app.role_permission directly while both TypeScript callers obey the rule — behaviour-identical today over all 540 pairs, 174 grants each way, zero divergence, and it would have told an organisation on a cloned role that it has no escalation path, in the product's own words. 0143 fixes both; assert_function_comment_claims_hold keeps them, with the VERB taken from the writer — "moves" forbids an UPDATE and not an INSERT, because fire_due_offer_waves creates an assignment already offered and creating is not moving. Third sweep — the reverse direction over the site, and §3 named a stylesheet the site does not serve while the forward direction had existed since 18 August. The real gap: on 18 August rules were RECOVERED from a deleted stylesheet and the recovery over-shot by five families — .accordion, the two dashboard shells, .avatar, .placeholder-note, .grid-2 — 16 rules shipped on every page for a fortnight styling nothing. site-audit.mjs §1b reports a RULE and never a class: .dash-foot was one selector of three whose other two style a LIVE .dash-label. Twenty-three sabotages across the three, five of them negative controls. ★ AND A SABOTAGE RESTORE DISCARDED THE WORK IT PROTECTED** — git checkout --, run 73's own fix for the /tmp/Pay.orig collision, reverted an uncommitted guard rewrite; the OLD pass message is the only reason it was caught
—The nine exclusivity claims, and the reverse over the console (run 75)
0144 + ac81158 · 2 Sep 2026
completeBoth items §3 named, and the measurement it asked for FIRST is what found the defects. Seventeen exclusivity sentences across 87 app.* comments; 0143 checks the four naming a table; every one of the other thirteen measured by hand and TWO WERE FALSE. ★ THE HANDOUT'S PROPOSED RULE COULD NOT HAVE WORKED — a *distinguishing predicate* cannot be INFERRED from a body, because actor_client_accounts is four lines each of which appears in dozens of siblings; what distinguishes it is a conjunction over one table, six referents, no two alike. ★ A DISCLOSURE ROW GUESSED WHICH CLIENT IT BELONGED TO. app.record_portal_view's S6/S7 branch spelled boundary two a second time and read it with a bare select … into — **the one construct that turns *more than one answer* into *an answer*** — while the comment directly above it says a two-account session is *"REFUSED RATHER THAN GUESSED AT … picking one would attribute a view to the wrong client"*, and the guard beneath it fires only on ZERO. Not an edge case: portal_client_accounts calls two accounts in one agency the normal case and client_user is unique on (client_account_id, user_account_id). Reachable from S6, the portal's FIRST screen, the only call site passing a null event. 0144 records what was actually disclosed — one row per account — through app.actor_client_accounts(), which makes *measured in one place* true rather than aspirational; a one-account session still writes one, asserted as a control. ★ AND A CALLER COUNT WAS A WHITELIST SIXTY LINES UNDER THE COMMENT EXPLAINING WHY WHITELISTS ROT. portal_client_accounts said *"Both portal_event and portal_events"* and four resolve through it; 0143's counter matches (one|two|…) callers?: and *"Both X and Y"* has no numeral in it. verify-V24.sh named the same two and asserted = 2 — now derived, printed, and required to contain the two §4 sabotages by name. ★ RULE A1 IS 78 PLACES, NOT ONE, AND 22 NAME THE RULE NOWHERE — almost exactly the money surface. verify-V23.sh's probe caught insufficient_privilege and called it GUARDED; so does the permission check three lines below rule A1. Measured: with event_pay's A1 guard replaced by if false, the old probe said GUARDED and the new one said *WRONG_REFUSAL you cannot read pay figures for this event*. Restored from a NUMBERED copy, verified byte-identical by sha256. The 22 are recorded, not rewritten — 1300 lines of unrelated logic frozen into a migration is a worse artefact than the defect. §4G, the reverse over web/src/design: §4E and §4F ask *does this class have a rule*; nothing asked the other way. ★ THE SITE'S READER PORTED STRAIGHT ACROSS CONDEMNS NINE LIVE RULES — every status chip in the product — because this tree BUILDS class names. Two fixes, each found by a false positive: a template swallows the literals inside its own holes, and text ending in - before a hole is a family prefix. 204 rules, 435 names, 5 families, zero dead. ★ AND THE FLOOR IS NOT THE COUNT — this reader's corpus cannot realistically collapse, so a pair of controls is planted on EVERY run: one selector nothing names, which must read dead, and one the reader itself claims to have seen, which must not. Left measured and unbuilt: six scripts print their sabotage catches after saying the control failed — observed live this run, six V24 sabotages reporting ok caught while quoting somebody else's assertion

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 named scripts/verify-V<n>.sh 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. Plus the standing gates: verify-policy-shape.sh, verify-design-parity.sh, verify-erasure.sh and verify-demo-absent.sh.

Run every gate at once:
for f in scripts/verify-V*.sh; do bash "$f" local || { echo "FAILED: $f"; break; }; done

Run it locally

# 1 · API worker
cd ~/Documents/event-clinic-suite/verbunk/api && npm run dev   # :8787
# 2 · console + site
cd ~/Documents/event-clinic-suite/verbunk/web && npm run dev   # :5173
# prove a slice on the committed tree
cd ~/Documents/event-clinic-suite/verbunk && bash scripts/verify-V10.sh local

Verbunk uses real router paths, so every link below is a genuine deep link. Port 5173 collides with event.clinic's web harness — start only one at a time, or the tiles render against the wrong API and every value shows a dash.

Screens with data

Screen
Open at
What is on it
Marketing site
public
http://localhost:5173/
Public site; no auth
Sign in
public
http://localhost:5173/sign-in
Session entry
Sign up
public
http://localhost:5173/sign-up
Client self-signup; landed in V5
Console home
V4 · data
http://localhost:5173/console
Shell, nav and the design system switched on
Clients
V5 · data
http://localhost:5173/console/clients
Client accounts; signup and invitations landed here
Events
V10 · data
http://localhost:5173/console/events
Events and workspace — V10, the newest slice, 72 checks green in one clean run
Workers
V6 · data
http://localhost:5173/console/workers
Worker profiles; 87 checks, 11 sabotage cases
People
V6 · data
http://localhost:5173/console/people
People behind the worker profiles
Roles
V3 · data
http://localhost:5173/console/roles
RBAC; 95 checks green
Credential types
V7/V8 · data
http://localhost:5173/console/credential-types
Documents and credentials with expiry blocking
Configuration
V9 · data
http://localhost:5173/console/configuration
Configuration packs; policy shape 372/0
Worker portal
V16 · shell only
http://localhost:5173/portal
The worker-facing surface
Accept invitation
needs a token
http://localhost:5173/accept-invitation
Needs a token in the URL — reached from an invitation email, not by hand
Requirement builder → Risk Radar
not started
does not exist yet
V11–V29 are pending. Nothing to open